基于有限约束的多线程并发BFS算法

BFS(广度优先搜索)是图论中的一个基础搜索算法,对于下图,BFS将按照节点的数字大小逐一遍历。

BFS-search.png

单线程中的实现

借用队列的先进先出特性实现,伪代码实现如下,代码清晰且易于理解。

 1 Breadth-First-Search(Graph, root):
 2 
 3     for each node n in Graph:            
 4         n.distance = INFINITY        
 5         n.parent = NIL
 6 
 7     create empty queue Q      
 8 
 9     root.distance = 0
10     Q.enqueue(root)                      
11 
12     while Q is not empty:        
13     
14         current = Q.dequeue()
15     
16         for each node n that is adjacent to current:
17             if n.distance == INFINITY:
18                 n.distance = current.distance + 1
19                 n.parent = current
20                 Q.enqueue(n)
21

多线程中的实现

很多搜索场景下,比如网络爬虫算法,单线程的BFS无法充分利用现有的多核多线程的优势。
算法导论第三版新增了一章多线程算法的内容,主要描述了在多线程环境下如何实现某种算法。主要思想可以通过递归计算斐波那契数列(Fibonacci)来描述,众所周知的fib递归算法如下:

def fib(n):
  if n < 2:
    return n
  a = fib(n - 1)
  b = fib(n - 2)
  return a + b

通过增加sync和spawn原语,可以将其转为多线程算法。spawn的含义代表在新线程中运行,sync的含义代表所有线程的汇聚,即所有线程都运行至此才继续执行下一行代码。

将上述fib转为多线程算法是这样的:

def fib(n):
  if n < 2:
    return n
  a = spawn fib(n - 1)
  b = fib(n - 2)
  sync
  return a + b

基于这个理论,需要首先找到哪些可并发,哪些需要汇聚,考虑下面这种情况:

bfs-multi-access.png

B和C,D、E和E是分别满足并发条件的,但B和C并发后的节点E同时属于二者的后继,如果多个线程同时访问时,会将E两次推送至造成错误,并有可能使得多个线程同时取得对E的访问。
在既保证遍历是按照广度优先,而又不至于发生多次访问的错误的情况下,可以采用一种基于有限约束的并发算法,即只允许当前节点的所有子节点并发。这种约束反馈在代码实现上:

 1 Breadth-First-Search(Graph, root):
 2-11         ... ...
12     while Q is not empty:        
13     
14         current = Q.dequeue()
15     
16         for spaw each node n that is adjacent to current:
17             if n.distance == INFINITY:
18                 n.distance = current.distance + 1
19                 n.parent = current
20                 Q.enqueue(n)
21         sync

注意上面for循环中的spaw,这里指的是对每个节点的访问都将在单独的线程中进行,仅当所有节点完成遍历后进行sync。这样,所有的并发既不会乱序,也不会带来访问冲突。

Actor模型实现spawn和sync

什么是Actor

七周七并发一书中详细介绍了Actor模型。首先它是一个通用并发编程模型,也被Erlang借鉴。其次,它封装了状态并通过消息与其他actor通信,可以适应共享内存架构和分布式内存架构,而且有很好的容错性。简单来讲,Actor就是一个可以接收和发送消息的异步执行体。

使用python来模拟Actor

鉴于Actor是一个可收发消息执行体,我们需要2个python的概念与其对应:threadqueue。 thread是一个执行体,而Queue恰好是一个消息存储体。

class Actor(Thread):
    def __init__(s):
        s.queue = Queue()
        Thread.__init__(s)
    def send(s, obj):
        s.queue.put(obj)
    def recv(s):
        return s.queue.get()
    def work_done(s):
        s.queue.task_done()
    def sync(s):
        s.queue.join()
    def has_more_work(s):
        return not s.queue.empty()
    def work():
        pass
    def run(s):
        s.work()

Python内置类Queue的join方法说明:

Queue.join()

Blocks until all items in the queue have been gotten and processed.

The count of unfinished tasks goes up whenever an item is added to the queue. The count goes down whenever a consumer thread calls task_done() to indicate that the item was retrieved and all work on it is complete. When the count of unfinished tasks drops to zero, join() unblocks.

配合task_done(),恰好可以模拟前文提及的sync方法,比如这样同步所有工作线程:

def worker():
    while True:
        item = q.get()
        do_work(item)
        q.task_done()
q = Queue()
for i in range(num_worker_threads):
     t = Thread(target=worker)
     t.daemon = True
     t.start()

for item in source():
    q.put(item)

q.join()       # block until all tasks are done

多线程BFS实现

有了可以sync的Actor,那么多线程的BFS就可以如下方式实现了。

首先将遍历节点的动作封装在Actor中,比如称之为Vistor,它是一个Actor:

class Visitor(Actor):
    def __init__(s, monitor, vid = 0):
        s.monitor = monitor
        s.sid = sid
        Actor.__init__(s)
    def work(s):
        while True:
            v = s.recv()
            nexts = visit(v)
            s.send(nexts)
            s.work_done()

对于发起BFS的线程,我们称之为monitor线程,当然,它也是一个Actor:

class Monitor(Actor):
    def __init__(s, n_visitors, start_vertex = [], depth = 8):
        Actor.__init__(s)
        s.n_visitors = n_visitors
        s.depth = depth
        s.choice = 0
        s.visit_history = set()
        for v in start_vertex:
            s.send(v)
        s.visitors = [Visitor(s, x) for x in range(n_visitors)]
        for sp in s.n_visitors:
            sp.start()
    def wait_all(s):
        for sp in s.n_visitors:
            sp.sync()
    def dispatch(s,v):
        if v not in s.visit_history:
            s.n_visitors[s.choice % s.n_visitors].send(v)
            s.choice += 1
            s.visit_history.add(v)
    
    def work(s):
        while s.has_more_work():
            for v in chain.from_iterable(s.recv_all()):
                if has_visit_depth == s.depth:
                    break
                s.dispatch(v)
            s.wait_all()
        s.done_work()

至此,我们就实现了这种基于有限约束的BFS了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容