原文:http://blog.ftofficer.com/2009/12/python-multiprocessing-2-object-sharing-across-process/
繼續寫關於Python
multiprocessing的使用手記,繼上次的進程模型之後,這次展開討論一下multiprocessing當中的跨進程對象共用的問題。
在mp庫當中,跨進程對象共用有三種方式,第一種僅適用於原生機器類型,即python.ctypes當中的類型,這種在mp庫的文檔當中稱為shared
memory
方式,即通過共用記憶體共用對象;另外一種稱之為server process
,
即有一個伺服器處理序負責維護所有的對象,而其他進程串連到該進程,通過代理對象動作伺服器進程當中的對象;最後一種在mp文檔當中沒有單獨提出,但是在其
中多次提到,而且是mp庫當中最重要的一種共用方式,稱為inheritance
,即繼承,對象在
父進程當中建立,然後在父進程是通過multiprocessing.Process建立子進程之後,子進程自動繼承了父進程當中的對象,並且子進程對這
些對象的操作都是反映到了同一個對象。
這三者共用方式各有特色,在這裡進行一些簡單的比較。
首先是共用方式所應對的物件類型,看這個表:
共用方式 |
支援的類型 |
Shared memory |
ctypes當中的類型,通過RawValue,RawArray等封裝類提供 |
Inheritance |
系統核心對象,以及基於這些對象實現的對象。包括Pipe, Queue, JoinableQueue, 同步對象(Semaphore, Lock, RLock, Condition, Event等等) |
Server process |
所有對象,可能需要自己手工提供代理對象(Proxy) |
這個表總結了三種不同的共用方式所支援的類型,下面一個個展開討論。
其中最單純簡單的就是shared
memory這種方式,只有ctypes當中的資料類型可以通過這種方式共用。由於mp庫本身缺少命名的機制,即在一個進程當中建立的對象,無法在另外一
個進程當中通過名字來引用,因此,這種共用方式依賴於繼承,對象應該由父進程建立,然後由子進程引用。關於這種機制的例子,可以參見Python文檔
當中的例子 Synchronization types like locks,
conditions and queues,參考其中的test_sharedvalues函數。
然後是繼承方式。首先關於繼承方式需要有說明,繼承本質上並不是一種對象共用的機制,對象共用只是其副作用。子進程從父進程繼承來的對象並不一定是
共用的。繼承本質上是父進程fork出的子進程自動繼承父進程的記憶體狀態和對象描述符。因此,實際上子進程複製
了一份
父進程的對象,只不過,當這個對象封裝了一些系統核心對象的描述符的時候,拷貝這個對象(及其封裝的描述符)實現了對象的共用。因此,在上面的表當中,只
有系統核心對象,和基於這些對象實現的對象,才能夠通過繼承來共用。通過繼承共用的對象在linux平台上沒有任何限制,但是在Windows上面由於沒
有fork的實現,因此有一些額外的限制條件
,因此,在Windows上面,繼承方式是幾乎無法用的。
最後就是Server Process這種方式。這種方式可以支援的類型比另外兩種都多,因為其模型是這樣的:
server process模型
在這個模型當中,有一個manager進程,負責管理實際的對象。真正的對象也是在manager進程的記憶體空間當中。所有需要訪問該對象的進程都
需要先串連到該管理進程,然後擷取到對象的一個代理對象(Proxy object),通常情況下,這個代理對象提供了實際對象的公用函
數
的代理,將函數參數進行pickle,然後通過串連傳送到管理進程當中,管理進程將參數unpickle之後,轉寄給相應的實際對象
的函數,傳回值(或者異常)同樣經過管理進程pickle之後,通過串連傳回到客戶進程,再由proxy對象進行unpickle,返回給調用者或者拋出
異常。
很明顯,這個模型是一個典型的RPC(遠端程序呼叫)的模型。因為每個客戶進程實際上都是在訪問manager進程當中的對象,因此完全可以通過這
個實現對象共用。
manager和proxy之間的串連可以是基於socket的網路連接,也可以是unix
pipe。如果是使用基於socket的串連方式,在使用proxy之前,需要調用manager對象的connect函數與遠端manager進程建
立串連。由於manager進程會開啟連接埠接收該串連,因此必要的身分識別驗證是需要的,否則任何人都可以連上manager弄亂你的共用對象。mp庫通過
authkey的方式來進行身分識別驗證。
在實現當中,manager進程通過multiprocessing.Manager類或者BaseManager的子類實現。
BaseManager提供了函數register註冊一個函數來擷取共用對象的proxy。這個函數會被客戶進程調用,然後在manager進程當中執
行。這個函數可以返回一個共用的對象(對所有的調用返回同一個對象),或者可以為每一個調用建立一個新的對象,通過前者就可以實現多個進程共用一個對象。
關於這個的用法可以參考Python文檔
當中的例子“Demonstration of how to create and
use customized managers and proxies”。
典型的匯出一個共用對象的代碼是:
ObjectType object_<br />class ObjectManager(multiprocessing.managers.BaseManager): pass<br />ObjectManager.register("object", lambda: object_)
注意上面介紹proxy對象的時候,我提到的“公用函數”四個字。每個proxy對象只會匯出實際對象的公用函數。這裡面有兩個含義,一個是“公
共”,即所有非底線開頭的成員,另一個是“函數”,即所有callable的成員。這就帶來一些限制,一是無法匯出屬性,二是無法匯出一些公用的特殊函
數,例如__get__,
__next__等等。對於這個mp庫有一套處理,即自訂proxy對象。首先是BaseManager的register可以提供一個
proxy_type作為第三個參數,這個參數指定了哪些成員需要被匯出。詳細的使用方法可以參見文檔當中的第一個例子。
另外manager還有一些細節的問題需要注意。由於Proxy對象不是安全執行緒的,因此如果需要在一個多線程程式當中使用proxy,mp庫會為
每個線程建立一個proxy對象,而每個proxy對象都會對server
process建立一個串連,而manager那邊對於每個串連都建立一個單獨的線程來為其服務。這樣帶來的問題就是,如果客戶進程有很多線程,很容易會
導致manager進程的fd數目達到ulimit的限制,即使沒有達到限制,也會因為manager進程當中有太多線程而嚴重影響manager的性
能。解決方案可以是一個進程內cache,只有一個單獨的線程可以建立proxy對象訪問共用對象,其餘線程只能訪問該進程當中的cache。
一旦manager因為達到ulimit限制或者其他異常,manager會直接退出,遺憾的是,這時候已經建立的proxy會試圖重新串連
manager – 但是它已經不存在了。這個會導致客戶進程hang在對proxy的函數調用上,這個時候,目前除了殺掉進程沒有找到別的辦法。
另外proxy使用socket的方式比較tricky,因此和內建的socket庫有很多衝突,比如
socket.setdefaulttimeout(Python Issue 6056
)。在setdefaulttimeout調用了之後,進程當中所有通過socket模組建立的socket都是被設定為unblock模式的,但是mp
庫並不知道這一點,而且它總是假設socket都是block模式的,於是,一旦調用了setdefaulttimeout,所有對於proxy的函數調
用都會拋出OSError,錯誤碼為11,錯誤原因是非常有誤導性的“Resource temporarily
unavailable”,實際上就是EAGAIN。這個錯誤可以通過我提供的一個patch
來補救(這個patch當中還包含其他的一些修複,所以請自行查看並修改該patch)。
由於以上的一些原因,server
process模式作為一個對象的共用模式,能夠提供最為靈活的共用方式,但是也有最多的問題。這個在使用過程當中就靠自己去衡量了。目前我們的系統對於
資料可靠性方面要求不高,遺失資料是可以接受的,但是也只用這種模式來維護統計值,不敢用來維護更多的東西。
關於跨進程共用對象的問題就寫到這裡,後面內容待續……