异步广度优先搜索算法

为什么要异步?

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,一经查实,立即删除!

相关文章

Servlet使用适配器模式进行增删改查案例(EmpServiceImpl.java)

/** * Title: EmpServiceImpl.java * Package org.service.impl * Description: TODO该方法的主要作用&#xff1a; * author A18ccms A18ccms_gmail_com * date 2017-9-10 下午8:33:06 * version V1.0 */ package org.service.impl;import java.util.List;import org.da…

Java读取properties配置文件时,中文乱码解决方法

转载自 关于java.util.Properties读取中文乱码的正确解决方案&#xff08;不要再用native2ascii.exe了&#xff09; 碰到了用java.util.Properties读取中文内容&#xff08;UTF-8格式&#xff09;的配置文件&#xff0c;发生中文乱码的现象&#xff0c; Properties propnew Pro…

jQuery 所有版本在线引用

https://www.cnblogs.com/xpwi/p/9806663.html jQuery 所有版本在线引用 jquery-3.1.1&#xff08;最新&#xff09; 官网jquery压缩版引用地址:<script src"https://code.jquery.com/jquery-3.1.1.min.js"></script> jquery-3.0.0 官网jquery压缩版…

Servlet使用适配器模式进行增删改查案例(jdbc.properties)

driveroracle.jdbc.driver.OracleDriver urljdbc\:oracle\:thin\:localhost\:1521\:orcl1 usernamescott password123

怎么用java实现通过身份证号码判断籍贯所在地区

https://blog.csdn.net/weixin_43876206/article/details/89426036 怎么用java实现通过身份证号码判断籍贯所在地区 java实现通过身份证号码判断籍贯所在地区 目标&#xff1a;用户输入自己的身份证号码&#xff0c;通过所输入的身份证号码来判断出用户的籍贯、所在地区。 功…

java-- properties总结

转载自 java-- properties总结 篇章一&#xff1a;Loading Properties from XML XML 属性文档具有以下 DOCTYPE 声明&#xff1a; <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> 注意&#xff0c;导入或导出属性时不 访问系统 URI…

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

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

List VS Set

Duang Duang Duang面试官来啦&#xff0c;快坐好&#xff0c;摆好姿势。 好了&#xff0c;我要开始提问了&#xff1a; 问&#xff1a;Set集合与List集合有什么区别呢&#xff1f; 答&#xff1a;Set集合中的数据不可重复&#xff0c;数据是无序的&#xff0c;List集合的数据可…

外部访问docker容器(docker run -p/-P 指令) docker run -d -p 5000:5000 {hostPort:containerPort(映射所有接口地}

https://www.cnblogs.com/williamjie/p/9915019.html &#xff08;2&#xff09;-p&#xff08;小写&#xff09;则可以指定要映射的IP和端口&#xff0c;但是在一个指定端口上只可以绑定一个容器。支持的格式有 hostPort:containerPort、ip:hostPort:containerPort、 ip::cont…

读取/书写Java的XML格式properties文件

转载自 读取/书写Java的XML格式properties文件 在JDK5中&#xff0c;properties文件的格式可以由XML构成&#xff0c;这里给出了一个读取/书写XML格式properties文件的例子。因为使用了XML&#xff0c;所以文件内容支持了CJKV(中文、日文、韩文、越南语)。可以直接书写、调用…

使用泛型前 VS 使用泛型后

不使用泛型&#xff1a; List list1 new ArrayList(); list1.add("www.educoder.net"); String str1 (String)list1.get(0);使用泛型&#xff1a; List<String> list2 new ArrayList<String>(); list2.add("www.educoder.net"); String s…

ssh报错java.lang.ClassCastException: com.sun.proxy.$Proxy6 cannot be cast to org.service.impl.EmpServi

错误如下&#xff1a; java.lang.ClassCastException: com.sun.proxy.$Proxy6 cannot be cast to org.service.impl.EmpServiceImpl at org.service.impl.EmpServiceImplTest.init(EmpServiceImplTest.java:44)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Metho…

去掉字符串后面所有的0 去掉字符串前面或后面的0;

https://blog.csdn.net/weixin_43844810/article/details/94577879 去掉字符串前面或后面的0&#xff1b; 养猫还是养狗&#xff1f; 2019-07-03 17:10:23 4492 收藏 1 分类专栏&#xff1a; 工作 版权 方法一&#xff1a; int a 0; //将油站编码前的0去掉 …

Java类加载器总结

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

ssh根据姓名查询的时候报错java.lang.IndexOutOfBoundsException: Remember that ordinal parameters are 1-based!

错误如下&#xff1a; java.lang.IndexOutOfBoundsException: Remember that ordinal parameters are 1-based! at org.hibernate.engine.query.ParameterMetadata.getOrdinalParameterDescriptor(ParameterMetadata.java:79)at org.hibernate.engine.query.ParameterMetadat…

拆分字符串

String.split()拆分字符串 lang包String类的split()方法 public String[] split(String regex) public String[] split(String regex&#xff0c;int limit) //limit 参数控制模式应用的次数&#xff0c;因此影响所得数组的长度 拆分示例:public class SplitDemo {public sta…

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…