异步广度优先搜索算法

为什么要异步?

CPU的工艺越来越小,Cannon Lake架构的Intel CPU已经达到10nm技术,因此在面积不变的情况下,核心数可以明显提升。单纯的提升主频将造成发热量大、需要的电压大、功耗大的问题。而传统的算法与数据结构是针对单核心单线程同步而言的,因此,传统的算法无法将CPU利用率达到最大。

广度优先搜索

首先我们先了解一下与之对应的深度优先搜索(DFS),深度优先搜索即像走迷宫时,始终保持左手与左侧墙壁接触,换言之即遇到岔路时永远向左拐,从而寻找出口。而广度优先搜索,则在每个岔路时,变出一个分身,继续前进。

但是,实际上是这样吗?答案是否定的,刚刚已经讲到,传统的算法与数据结构是建立在单线程同步基础上的,因此传统算法只能够模拟分身在同时前进,这时就要引入队列来保存和展开岔路节点(当遇到新岔路时,将这个节点放入队列。队列头部元素进行展开寻找新的岔路并放入队列尾部)。

基于Parallel的并行广度优先搜索

而在并行或异步以及多线程的环境下,我们可以真的让“分身”们同时前进。首先使用并行广度优先搜索的前提是你不在意真的保证了广度是同步的,虽然并行广度优先搜索能够寻找到全部解,但是无法保证同一时刻进行搜索的任务是在同一深度的。

在这一前提下,我们以遍历图为例,首先定义邻接表的数据结构:

public class Node{    public string Value { get; set; }    
public LinkedList<Node> Nodes { get; set; } =
       new LinkedList<Node>(); }

假设我们的图结构如下:


进行数据的初始化:

public static void Main(string[] args){   
  var A = new Node { Value = "A" };    
   var B = new Node { Value = "B" };    
   var C = new Node { Value = "C" };
  var D = new Node { Value = "D" };  
   var E = new Node { Value = "E" };    
   var F = new Node { Value = "F" };    
   var G = new Node { Value = "G" };A.Nodes.AddLast(B);A.Nodes.AddLast(C);A.Nodes.AddLast(D);B.Nodes.AddLast(A);B.Nodes.AddLast(D);C.Nodes.AddLast(A);D.Nodes.AddLast(A);D.Nodes.AddLast(B);D.Nodes.AddLast(E);E.Nodes.AddLast(D);E.Nodes.AddLast(F);F.Nodes.AddLast(E);F.Nodes.AddLast(G);G.Nodes.AddLast(F);    
   // TODO: Async visit}

在此处,姑且认为Node.GetHashCode()可以作为Node的唯一标识,我们来定义一个HashSet来存储已经访问过的Node标识:

private static HashSet<int> Visited = new HashSet<int>();

此时,我们只需要按照正常编写深度优先搜索的递归方法编写即可,但其中的循环使用Parallel提供的循环方法,这样即可实现广度搜索:

public void Visit(Node n){    
lock (Visited){        if (Visited.Contains(n.GetHashCode())){            return;}Visited.Add(n.GetHashCode());}Console.WriteLine($"{ n.Value } ");Parallel.ForEach(n.Nodes, x => {Visit(x);}); }

基于Task的异步广度优先搜索

如果我们需要在进行搜索时,保持同一个时间点的任务所涉及到的节点的深度一致,我们就需要将上述方法改写成异步方式,并使用异步信号量来使处于同一深度的Task等待同深度其他Task完成:

首先定义一个异步信号控制器类AsyncSemaphore,其中包含一个公共构造方法和两个公共方法:

public class AsyncSemaphore{    public void AddTaskCount(int)    public Task WaitAsync();    public void Release();
}

该类被初始化时,认为需要等待的任务数量为0,通过调用AddTaskCount来增加需要等待的任务数,WaitAsync被调用时,将先判断需要等待的任务数量是否与已经完成的任务数量相等,如果相等则不等待,不相等则返回一个等待信号。当Release被调用后,判断需要等待的任务数量是否与已经完成的任务数量相等,如果相等则置所有等待信号放行。

因此这个类的具体实现如下:

public class AsyncSemaphore{  

 private int m_totalCount = 0;  
 private int m_finishedCount = 0;  
 
   private readonly List<TaskCompletionSource<bool>> m_waiters =
    new List<TaskCompletionSource<bool>>();  
   
   private readonly static Task s_completed =
                         Task.FromResult(true);    
   
   public void AddTaskCount(int count)    {m_totalCount += count;}  
   
    public Task WaitAsync()    {    
       lock (m_waiters){          
           if (m_finishedCount == m_totalCount){          
                 return s_completed;}          
             else{        
                var waiter = new TaskCompletionSource<bool>();m_waiters.Add(waiter);          
                 return waiter.Task;}}}  
 
    public void Release()    {        lock (m_waiters){m_finishedCount++;      
            if (m_finishedCount == m_totalCount){Parallel.ForEach(m_waiters, x => {x.SetResult(true);});m_waiters.Clear();}}} }

在编写Visit方法之前,我们需要对每个深度设置一个锁,因此我们需要定义一个Dictionary来存储各个深度(或叫层)的锁:

private static Dictionary<int, AsyncSemaphore> Lockers = 
new Dictionary<int, AsyncSemaphore>();

同时,需要为起点层预设一个锁:

Lockers.Add(0, new AsyncSemaphore());
Lockers[0].AddTaskCount(1);

接下来编写VisitAsync方法,该方法是一个异步函数,第一个参数接收节点,第二个参数为当前深度,起点深度为0。

public static async Task VisitAsync(Node n, int deep = 0)

在VisitAsync方法被调用时,应先检查该节点是否被访问:

lock (Visited) {   
   if (Visited.Contains(n.GetHashCode()))    {        Lockers[deep].Release();
return;}Visited.Add(n.GetHashCode()); }

这里需要额外说明的一点就是,如果这个节点被访问过,也是需要释放锁的。因为在后面的节点展开代码中,我们并没有过滤节点是否被访问过,因此访问过的节点也包含在了AddTaskCount()的参数中。

接下来,我们需要检查下一层的锁有无被初始化:

lock(Lockers) {    if (!Lockers.ContainsKey(deep + 1))    {        Lockers.Add(deep + 1, new AsyncSemaphore());}
}

这些准备工作完成后,即可输出当前节点的Value,输出后,我们计算一下当前节点有多少子节点,将这个数值累加到下一层的异步锁中,添加完毕后,通知本层锁已经完成了一个任务并等待本层其他任务完成后,继续展开本节点:

Console.Write($"{ n.Value } ");
Lockers[deep + 1].AddTaskCount(n.Nodes.Count);
Lockers[deep].Release();
await Lockers[deep].WaitAsync();
Parallel.ForEach(n.Nodes, x => {VisitAsync(x, deep + 1);
});

运行调试时,我们可以观察到A永远是第一个输出的,结尾顺序永远是EFG,而第二层的顺序是不固定的。因此证明了广度优先搜索是成功的。以上即为基于异步实现的广度优先搜索。

原文地址:http://www.1234.sh/post/async-bfs-algorithm


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327116.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开箱即用 - jwt 无状态分布式授权

基于JWT(Json Web Token)的授权方式 JWT 是JSON风格轻量级的授权和身份认证规范&#xff0c;可实现无状态、分布式的Web应用授权&#xff1b; 从客户端请求服务器获取token&#xff0c; 用该token 去访问实现了jwt认证的web服务器。 token 可保存自定义信息&#xff0c;如用户基…

Java类加载器总结

转载自 Java类加载器总结 1.类的加载过程 JVM将类加载过程分为三个步骤&#xff1a;装载&#xff08;Load&#xff09;&#xff0c;链接&#xff08;Link&#xff09;和初始化(Initialize)链接又分为三个步骤&#xff0c;如下图所示&#xff1a; 1) 装载&#xff1a;查找并…

MyBatis-Plus EntityWrapper的使用 wrapper le ge

https://blog.csdn.net/shujuelin/article/details/99568651 MyBatis-Plus EntityWrapper的使用 脚丫先生 2019-08-14 14:43:43 2660 收藏 分类专栏&#xff1a; javaee 版权 调度Airflow 本专刊主要以调度系统Airflow详细讲解(会把工作中对于调度系统的docker容器化部署、…

又踩.NET Core的坑:在同步方法中调用异步方法Wait时发生死锁(deadlock)

之前在将 Memcached 客户端 EnyimMemcached 迁移 .NET Core 时被这个“坑”坑的刻骨铭心&#xff08;详见以下链接&#xff09;&#xff0c;当时以为只是在构造函数中调用异步方法&#xff08;注&#xff1a;这里的异步方法都是指基于Task的&#xff09;才会出线死锁&#xff0…

jvm类加载器以及双亲委派

转载自 jvm类加载器以及双亲委派 首先来了解几个概念&#xff1a; 类加载&#xff1a; 概念&#xff1a;虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验--转换解析--初始化&#xff0c;最终形成能被java虚拟机直接使用的java类型&#xff0c;就是jvm…

分布式系统搭建:服务发现揭秘

CAP理论 加州大学终身教授与著名计算机科学家Eric Allen Brewer在90年代末提出了CAP理论&#xff0c;理论断言任一个基于网络的分布式系统&#xff0c;最多只能满足“数据一致性”、“可用性”、“分区容错性”三要素中的两个要素。 该理论后被MIT证明可行&#xff0c;故架构师…

SSL / TLS 协议运行机制详解

转载自 SSL / TLS 协议运行机制详解 互联网的通信安全&#xff0c;建立在SSL/TLS协议之上。 本文简要介绍SSL/TLS协议的运行机制。文章的重点是设计思想和运行过程&#xff0c;不涉及具体的实现细节。如果想了解这方面的内容&#xff0c;请参阅RFC文档。 一、作用 不使用SS…

在ASP.NET Core Web API上使用Swagger提供API文档

我在开发自己的博客系统&#xff08;http://daxnet.me&#xff09;时&#xff0c;给自己的RESTful服务增加了基于Swagger的API文档功能。当设置IISExpress的默认启动路由到Swagger的API文档页面后&#xff0c;在IISExpress启动Web API站点后&#xff0c;会自动重定向到API文档页…

一文告诉你 Java RMI 和 RPC 的区别

转载自 一文告诉你 Java RMI 和 RPC 的区别 RPC 远程过程调用 RPC&#xff08;Remote Procedure Call Protocol&#xff09;远程过程调用协议&#xff0c;通过网络从远程计算机上请求调用某种服务。一次RPC调用的过程大概有10步&#xff1a; 1.执行客户端调用语句&#xff…

Java架构师必须知道的 6 大设计原则

转载自 Java架构师必须知道的 6 大设计原则 在软件开发中&#xff0c;前人对软件系统的设计和开发总结了一些原则和模式&#xff0c; 不管用什么语言做开发&#xff0c;都将对我们系统设计和开发提供指导意义。本文主要将总结这些常见的原则&#xff0c;和具体阐述意义。 开发…

Flux --gt; Redux --gt; Redux React 入门 基础实例教程

本文的目的很简单&#xff0c;介绍Redux相关概念用法 及其在React项目中的基本使用 假设你会一些ES6、会一些React、有看过Redux相关的文章&#xff0c;这篇入门小文应该能帮助你理一下相关的知识 一般来说&#xff0c;推荐使用 ES6ReactWebpack 的开发模式&#xff0c;但Webpa…

mybatisplus 强制制空 空覆盖原来的字符串

ApiModelProperty(value "证件照片url") TableField(value "id_photo_url",fill FieldFill.UPDATE) private String idPhotoUrl; 方法一 Data EqualsAndHashCode(callSuper false) Accessors(chain true) TableName("base_party_member") A…

微软开源Visual Studio测试平台VSTest

IT之家1月21日消息 微软在MSDN博客上宣布&#xff0c;开源旗下Visual Studio测试平台VSTest。这一平台是具备高扩展性的单元测试执行框架&#xff0c;能够在不同的核心之间实现并行化&#xff0c;提供进程隔离&#xff0c;并能够整合进Visual Studio。 目前&#xff0c;VSTest能…

线程的状态与调度

当我们使用new关键字新建一个线程&#xff0c;这个时候线程就进入了新建状态&#xff08;New&#xff09;&#xff0c;也就是图中未启动状态&#xff1b;调用start方法启动线程&#xff0c;这个时候就进入了可运行状态&#xff0c;也就是就绪状态&#xff08;Runnable&#xff…

深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

转载自 深入JVM系列&#xff08;三&#xff09;之类加载、类加载器、双亲委派机制与常见问题 一&#xff0e;概述 定义&#xff1a;虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验、转换解析和初始化&#xff0c;最终形成可以被虚拟机直接使用的java…

Fabio 安装和简单使用

Fabio&#xff08;Go 语言&#xff09;&#xff1a;https://github.com/eBay/fabio Fabio 是一个快速、现代、zero-conf 负载均衡 HTTP(S) 路由器&#xff0c;用于部署 Consul 管理的微服务。 Fabio 由 eBay Classifieds Group 开发&#xff0c;用于处理 marktplaats.nl 和 kij…

计算密集型分布式内存存储和运算平台架构

1. 相关概念 1.1 内存数据库 关系型数据库处理永久、稳定的数据&#xff0c;内存数据库就是将其数据放在内存中&#xff0c;活动事务只与内存数据打交道&#xff0c;重新设计了体系结构并且在数据缓存、快速算法、并行操作方面也进行了相应的改进&#xff0c;所以数据处理速度比…

【深入Java虚拟机】之四:类加载机制

转载自 【深入Java虚拟机】之四&#xff1a;类加载机制 类加载过程 类从被加载到虚拟机内存中开始&#xff0c;到卸载出内存为止&#xff0c;它的整个生命周期包括&#xff1a;加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示&#xff1a; 其中…

违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制

转载自 违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制 前言&#xff1a; 本文是基于 ClassLoader双亲委派机制源码分析 了解过正统JDK类加载机制及其实现原理的基础上&#xff0c;进而分析这种思想如何应用到Tomcat这个web容器中&#xff0c;从源码的角度对 违…