基于有限約束的多線程并發(fā)BFS算法

BFS(廣度優(yōu)先搜索)是圖論中的一個(gè)基礎(chǔ)搜索算法坡贺,對(duì)于下圖,BFS將按照節(jié)點(diǎn)的數(shù)字大小逐一遍歷。

BFS-search.png

單線程中的實(shí)現(xiàn)

借用隊(duì)列的先進(jìn)先出特性實(shí)現(xiàn)器贩,偽代碼實(shí)現(xiàn)如下,代碼清晰且易于理解朋截。

 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

多線程中的實(shí)現(xiàn)

很多搜索場(chǎng)景下蛹稍,比如網(wǎng)絡(luò)爬蟲算法,單線程的BFS無法充分利用現(xiàn)有的多核多線程的優(yōu)勢(shì)部服。
算法導(dǎo)論第三版新增了一章多線程算法的內(nèi)容唆姐,主要描述了在多線程環(huán)境下如何實(shí)現(xiàn)某種算法。主要思想可以通過遞歸計(jì)算斐波那契數(shù)列(Fibonacci)來描述廓八,眾所周知的fib遞歸算法如下:

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

通過增加sync和spawn原語奉芦,可以將其轉(zhuǎn)為多線程算法。spawn的含義代表在新線程中運(yùn)行剧蹂,sync的含義代表所有線程的匯聚声功,即所有線程都運(yùn)行至此才繼續(xù)執(zhí)行下一行代碼。

將上述fib轉(zhuǎn)為多線程算法是這樣的:

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

基于這個(gè)理論宠叼,需要首先找到哪些可并發(fā)先巴,哪些需要匯聚,考慮下面這種情況:

bfs-multi-access.png

B和C,D伸蚯、E和E是分別滿足并發(fā)條件的醋闭,但B和C并發(fā)后的節(jié)點(diǎn)E同時(shí)屬于二者的后繼,如果多個(gè)線程同時(shí)訪問時(shí)朝卒,會(huì)將E兩次推送至造成錯(cuò)誤证逻,并有可能使得多個(gè)線程同時(shí)取得對(duì)E的訪問。
在既保證遍歷是按照廣度優(yōu)先抗斤,而又不至于發(fā)生多次訪問的錯(cuò)誤的情況下囚企,可以采用一種基于有限約束的并發(fā)算法,即只允許當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)并發(fā)瑞眼。這種約束反饋在代碼實(shí)現(xiàn)上:

 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循環(huán)中的spaw龙宏,這里指的是對(duì)每個(gè)節(jié)點(diǎn)的訪問都將在單獨(dú)的線程中進(jìn)行,僅當(dāng)所有節(jié)點(diǎn)完成遍歷后進(jìn)行sync伤疙。這樣银酗,所有的并發(fā)既不會(huì)亂序,也不會(huì)帶來訪問沖突徒像。

Actor模型實(shí)現(xiàn)spawn和sync

什么是Actor

七周七并發(fā)一書中詳細(xì)介紹了Actor模型黍特。首先它是一個(gè)通用并發(fā)編程模型,也被Erlang借鑒锯蛀。其次灭衷,它封裝了狀態(tài)并通過消息與其他actor通信,可以適應(yīng)共享內(nèi)存架構(gòu)和分布式內(nèi)存架構(gòu)旁涤,而且有很好的容錯(cuò)性翔曲。簡(jiǎn)單來講,Actor就是一個(gè)可以接收和發(fā)送消息的異步執(zhí)行體劈愚。

使用python來模擬Actor

鑒于Actor是一個(gè)可收發(fā)消息執(zhí)行體瞳遍,我們需要2個(gè)python的概念與其對(duì)應(yīng):threadqueue。 thread是一個(gè)執(zhí)行體菌羽,而Queue恰好是一個(gè)消息存儲(chǔ)體掠械。

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內(nèi)置類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實(shí)現(xiàn)

有了可以sync的Actor份蝴,那么多線程的BFS就可以如下方式實(shí)現(xiàn)了。

首先將遍歷節(jié)點(diǎn)的動(dòng)作封裝在Actor中氓轰,比如稱之為Vistor婚夫,它是一個(gè)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()

對(duì)于發(fā)起B(yǎng)FS的線程,我們稱之為monitor線程署鸡,當(dāng)然案糙,它也是一個(gè)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()

至此限嫌,我們就實(shí)現(xiàn)了這種基于有限約束的BFS了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末时捌,一起剝皮案震驚了整個(gè)濱河市怒医,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奢讨,老刑警劉巖稚叹,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拿诸,居然都是意外死亡扒袖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門亩码,熙熙樓的掌柜王于貴愁眉苦臉地迎上來季率,“玉大人,你說我怎么就攤上這事描沟§海” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵吏廉,是天一觀的道長(zhǎng)泞遗。 經(jīng)常有香客問我,道長(zhǎng)迟蜜,這世上最難降的妖魔是什么刹孔? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮娜睛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卦睹。我一直安慰自己畦戒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布结序。 她就那樣靜靜地躺著障斋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪徐鹤。 梳的紋絲不亂的頭發(fā)上垃环,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音返敬,去河邊找鬼遂庄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛劲赠,可吹牛的內(nèi)容都是我干的涛目。 我是一名探鬼主播秸谢,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼霹肝!你這毒婦竟也來了估蹄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤沫换,失蹤者是張志新(化名)和其女友劉穎臭蚁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讯赏,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刊棕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了待逞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甥角。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖识樱,靈堂內(nèi)的尸體忽然破棺而出嗤无,到底是詐尸還是另有隱情,我是刑警寧澤怜庸,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布当犯,位于F島的核電站,受9級(jí)特大地震影響割疾,放射性物質(zhì)發(fā)生泄漏嚎卫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一宏榕、第九天 我趴在偏房一處隱蔽的房頂上張望拓诸。 院中可真熱鬧,春花似錦麻昼、人聲如沸奠支。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倍谜。三九已至,卻和暖如春叉抡,著一層夾襖步出監(jiān)牢的瞬間尔崔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工褥民, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留季春,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓轴捎,卻偏偏與公主長(zhǎng)得像鹤盒,于是被迫代替她去往敵國(guó)和親蚕脏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容