原文:http://blog.ftofficer.com/2009/12/python-multiprocessing-3-about-queue/
繼續討論Python multiprocessing,這次討論的主要內容是mp庫的核心組件之一的Queue。
Queue是mp庫當中用來提供多進程對象交換的方式。對象交換和上一部分
當中提到的對象共用都是使多個進程訪問同一個對象的方式,兩者的區別就是,對象共用是多個進程訪問同一
個對象,對象交換則是將對象從一個進程傳輸的另一個進程。
multiprocessing當中的Queue使用方式和Python內建的threading.Queue對象很像,它支援一個put操作,將
對象放入Queue,也支援一個get操作,將對象從Queue當中讀出。和threading.Queue不同的是,mp.Queue預設不支援
join()和task_done操作,這兩個支援需要使用mp.JoinableQueue對象。
由於Queue對象負責進程之間的對象傳輸,因此第一個問題就是如何在兩個進程之間共用這個Queue對象本身。在上一部分所言的三種共用方式當
中,Queue對象只能使用繼承(inheritance)的方式共用。這是因為Queue本身基於unix的Pipe對象實現,而Pipe對象的共用需
要通過繼承。因此,在一個典型的應用實現模型當中,應該是父進程建立Queue,然後建立子進程共用該Queue,由父進程和子進程分別讀寫。例如下面的
這個例子:
import multiprocessing</p><p>q = multiprocessing.Queue()</p><p>def reader_proc():<br /> print q.get()</p><p>reader = multiprocessing.Process(target=reader_proc)<br />reader.start()</p><p>q.put(100)<br />reader.join()
另一種實現方式是父進程建立Queue,建立多個子進程,有的子進程讀Queue,有的子進程寫Queue,例如:
import multiprocessing</p><p>q = multiprocessing.Queue()</p><p>def writer_proc():<br /> q.put(100)</p><p>def reader_proc():<br /> print q.get()</p><p>reader = multiprocessing.Process(target=reader_proc)<br />reader.start()<br />writer = multiprocessing.Process(target=writer_proc)<br />writer.start()</p><p>reader.join()<br />writer.join()
由於使用繼承的方式共用Queue,因此代碼當中並沒有明顯的傳輸Queue對象本身的代碼,看起來似乎只要將multiprocessing當中
的對象換成threading當中的對象,程式仍然能夠工作。反之,拿到一個現有的多線程程式,是不是將threading改成
multiprocessing就可以工作呢?也許可以,但是更可能的情況是你會遇到很多問題。
第一個問題就是mp的Queue需要考慮多進程之間的對象傳輸,因此所傳輸的對象必須是可以pickle的。否則,在Queue的put操作上會拋
出PicklingError。
其他的一些差異表現在一些技術細節上,這些不是任何高層邏輯可以抽象掉的,不知道這些差異會導致一些潛在的錯誤,例如死結。在總結這些潛在的犯錯的
可能的同時,我們會簡單看一下mp當中Queue的實現方式,以便能夠方便的理解為什麼會有這樣的行為。這些實現問題僅僅針對Linux,Windows
上面的實現和出現的問題在這裡不涉及。
mp.Queue建構在系統的Pipe之上,但是實際上進程並不是直接將對象寫入到Pipe裡面,而是先寫入一個本地的buffer,再由一個專門
的feed線程將其放入Pipe當中。讀取端則是直接從Pipe當中讀出對象。之所以有這樣一個feed線程,是為了能夠提供Queue介面函數所需要的
put的逾時控制。但是由於這個feed線程的存在,mp.Queue提供了幾個額外的函數來控制它,一個函數close來停止該線程,以及
join_thread來join該線程。close同時負責把所有在buffer當中的對象重新整理到Pipe當中。
但是這個feed線程也是個麻煩製造者,為了保證所有被放入Queue的東西最終都能夠到達另外一端的進程,mp庫註冊了一個atexit的處理函
數,用來在進程退出的時候自動close並且join該feed線程。這個join動作帶來了很多問題,比如潛在的死結。考慮下面一種狀況:一個父進程創
建了兩個子進程,一個子進程讀,另一個子進程寫。當需要停止這些進程的時候,父進程如果先把讀進程結束,但是同時寫進程已經將太多的對象寫入Queue,
導致後繼的對象等待在buffer當中,則這個進程將無法終止,因為atexit的處理函數等待把所有buffer當中的對象放入Pipe,但是Pipe
已經滿了,然後陷入了死結。
有人可能會問,那隻要保證總是按照資料流的順序來停止進程不就行。問題是在很多複雜的系統流程當中,可能存在一個環形的資料流,這種情況下,無論按
照什麼順序停止進程,終究有一個進程可能陷入這種情景當中。
幸運的是,Queue對象還提供了一個成員函數cancel_join_thread,這個函數可以使得在進程停止的時候不進行join操作,這樣
可以避免死結,代價就是這個時候尚未重新整理到Pipe當中的對象都會丟失。鑒於即使調用了join_thread,殘留在Pipe當中的對象仍然可能丟失,
所以一旦選擇使用mp的Queue對象,就不要假設不會在流程當中丟對象了。
另外一個可能的方案是使用mp庫當中的SimpleQueue對象。這個對象在文檔當中沒有提及,但是在
multiprocessing.queue模組當中有定義。這個對象就是去掉了buffer的Queue對象,因此可能
能
夠避免上面說的問題的。但是SimpleQueue沒有提供put和get的逾時處理,兩個動作都是阻塞的。
除了使用multiprocessing.Queue,還可以使用multiprocessing.Pipe進行通訊。mp.Pipe是Queue
的底層結構,但是沒有feed線程和put/get的逾時控制。一定程度上和SimpleQueue很像。需要注意的是Pipe帶有一個參數
duplex,當設定為True(預設)的時候,Pipe並不是使用系統的pipe來實現,而是通過socketpair,即Unix Domain
Socket來實現。這個和pipe相比有些微的效能差異。
另外一個使用Queue的方式不是mp庫內建的。這種方式使用上一篇文章當中提到的server
process的方式來共用一個Queue對象。這個Queue對象實際上在server
process當中,所有的子進程通過socket串連到server
process擷取該Queue的代理對象進行操作。說到這有人會想起來mp庫有一個內建的SyncManager對象
,可以通過multiprocess.Manager函數擷取到,通過該對象的
Queue方法可以擷取一個Queue的代理對象。不幸的是,這個方法不是正確的擷取Queue的方式,原因正如上一篇文章所
說,SyncManager.Queue方法的每次調用擷取到的是一個建立對象的代理對象,而不是一個共用對象。正確的使用server
process當中的Queue的方式是:
共同部分:
import multiprocessing.managers as mpm<br />import Queue</p><p>class SharedQueueManager(mpm.BaseManager): pass<br />q = Queue.Queue()<br />SharedQueueManager.register('Queue', lambda: q)
服務進程:
mgr = SharedQueueManager(address=('', 12345))<br />server = mgr.get_server()<br />server.serve_forever()
客戶進程:
mgr = SharedQueueManager(address=('localhost', 12345))<br />mgr.connect()<br />q = mgr.Queue() # 這裡q就是共用的Queue對象的代理對象
這種方式比起mp庫內建的Queue,有一些效能上的影響,因為畢竟牽涉到多次網路通訊,但是帶來的好處是沒有feed線程帶來的一系列問題,而且
理論上不會存在丟資料的問題,除非server process崩潰。但是正如上一篇所說,server
process本身就不是很靠譜的,因此這裡也只是“理論上”不會丟資料而已。
說到效能,這裡就列兩個效能資料,以前在twitter上面提到
過的
(這兩個串連無法訪問的請聯絡我):
操作對象為
pickle後512位元組的對象,通過proxy操作Queue的效能大約是7000次/秒(本機)或1100次/秒(多機),如果使用
multiprocessing.Queue,效率可達54000次/秒。