轉(zhuǎn)自張聰?shù)腷log
繼續(xù)寫關(guān)于Python multiprocessing的使用手記,繼上次的進(jìn)程模型之后泊交,這次展開討論一下multiprocessing當(dāng)中的跨進(jìn)程對象共享的問題雁歌。
在mp庫當(dāng)中宏浩,跨進(jìn)程對象共享有三種方式,第一種僅適用于原生機器類型靠瞎,即python.ctypes當(dāng)中的類型比庄,這種在mp庫的文檔當(dāng)中稱為shared memory方式,即通過共享內(nèi)存共享對象乏盐;另外一種稱之為server process佳窑,即有一個服務(wù)器進(jìn)程負(fù)責(zé)維護(hù)所有的對象,而其他進(jìn)程連接到該進(jìn)程父能,通過代理對象操作服務(wù)器進(jìn)程當(dāng)中的對象神凑;最后一種在mp文檔當(dāng)中沒有單獨提出,但是在其中多次提到法竞,而且是mp庫當(dāng)中最重要的一種共享方式耙厚,稱為inheritance强挫,即繼承,對象在父進(jìn)程當(dāng)中創(chuàng)建薛躬,然后在父進(jìn)程是通過multiprocessing.Process創(chuàng)建子進(jìn)程之后俯渤,子進(jìn)程自動繼承了父進(jìn)程當(dāng)中的對象,并且子進(jìn)程對這些對象的操作都是反映到了同一個對象型宝。
這三者共享方式各有特色八匠,在這里進(jìn)行一些簡單的比較。
首先是共享方式所應(yīng)對的對象類型趴酣,看這個表:
共享方式 | 支持的類型 |
---|---|
Shared memory | ctypes當(dāng)中的類型梨树,通過RawValue,RawArray等包裝類提供 |
Inheritance | 系統(tǒng)內(nèi)核對象岖寞,以及基于這些對象實現(xiàn)的對象抡四。包括Pipe, Queue, JoinableQueue, 同步對象(Semaphore, Lock, RLock, Condition, Event等等) |
Server process | 所有對象,可能需要自己手工提供代理對象(Proxy) |
這個表總結(jié)了三種不同的共享方式所支持的類型仗谆,下面一個個展開討論指巡。
其中最單純簡單的就是shared memory這種方式,只有ctypes當(dāng)中的數(shù)據(jù)類型可以通過這種方式共享隶垮。由于mp庫本身缺少命名的機制藻雪,即在一個進(jìn)程當(dāng)中創(chuàng)建的對象,無法在另外一個進(jìn)程當(dāng)中通過名字來引用狸吞,因此勉耀,這種共享方式依賴于繼承,對象應(yīng)該由父進(jìn)程創(chuàng)建蹋偏,然后由子進(jìn)程引用便斥。關(guān)于這種機制的例子,可以參見Python文檔當(dāng)中的例子 Synchronization types like locks, conditions and queues暖侨,參考其中的test_sharedvalues函數(shù)椭住。
然后是繼承方式崇渗。首先關(guān)于繼承方式需要有說明字逗,繼承本質(zhì)上并不是一種對象共享的機制,對象共享只是其副作用宅广。子進(jìn)程從父進(jìn)程繼承來的對象并不一定是共享的葫掉。繼承本質(zhì)上是父進(jìn)程fork出的子進(jìn)程自動繼承父進(jìn)程的內(nèi)存狀態(tài)和對象描述符。因此跟狱,實際上子進(jìn)程復(fù)制了一份父進(jìn)程的對象俭厚,只不過,當(dāng)這個對象包裝了一些系統(tǒng)內(nèi)核對象的描述符的時候驶臊,拷貝這個對象(及其包裝的描述符)實現(xiàn)了對象的共享挪挤。因此叼丑,在上面的表當(dāng)中,只有系統(tǒng)內(nèi)核對象扛门,和基于這些對象實現(xiàn)的對象鸠信,才能夠通過繼承來共享。通過繼承共享的對象在linux平臺上沒有任何限制论寨,但是在Windows上面由于沒有fork的實現(xiàn)星立,因此有一些額外的限制條件,因此葬凳,在Windows上面绰垂,繼承方式是幾乎無法用的。
最后就是Server Process這種方式火焰。這種方式可以支持的類型比另外兩種都多劲装,因為其模型是這樣的:
server process模型
在這個模型當(dāng)中,有一個manager進(jìn)程昌简,負(fù)責(zé)管理實際的對象酱畅。真正的對象也是在manager進(jìn)程的內(nèi)存空間當(dāng)中。所有需要訪問該對象的進(jìn)程都需要先連接到該管理進(jìn)程江场,然后獲取到對象的一個代理對象(Proxy object)纺酸,通常情況下,這個代理對象提供了實際對象的公共函數(shù)的代理址否,將函數(shù)參數(shù)進(jìn)行pickle餐蔬,然后通過連接傳送到管理進(jìn)程當(dāng)中,管理進(jìn)程將參數(shù)unpickle之后佑附,轉(zhuǎn)發(fā)給相應(yīng)的實際對象的函數(shù)樊诺,返回值(或者異常)同樣經(jīng)過管理進(jìn)程pickle之后,通過連接傳回到客戶進(jìn)程音同,再由proxy對象進(jìn)行unpickle词爬,返回給調(diào)用者或者拋出異常。
很明顯权均,這個模型是一個典型的RPC(遠(yuǎn)程過程調(diào)用)的模型顿膨。因為每個客戶進(jìn)程實際上都是在訪問manager進(jìn)程當(dāng)中的對象,因此完全可以通過這個實現(xiàn)對象共享叽赊。
manager和proxy之間的連接可以是基于socket的網(wǎng)絡(luò)連接恋沃,也可以是unix pipe。如果是使用基于socket的連接方式必指,在使用proxy之前囊咏,需要調(diào)用manager對象的connect函數(shù)與遠(yuǎn)程的manager進(jìn)程建立連接。由于manager進(jìn)程會打開端口接收該連接,因此必要的身份驗證是需要的梅割,否則任何人都可以連上manager弄亂你的共享對象霜第。mp庫通過authkey的方式來進(jìn)行身份驗證。
在實現(xiàn)當(dāng)中户辞,manager進(jìn)程通過multiprocessing.Manager類或者BaseManager的子類實現(xiàn)庶诡。BaseManager提供了函數(shù)register注冊一個函數(shù)來獲取共享對象的proxy。這個函數(shù)會被客戶進(jìn)程調(diào)用咆课,然后在manager進(jìn)程當(dāng)中執(zhí)行末誓。這個函數(shù)可以返回一個共享的對象(對所有的調(diào)用返回同一個對象),或者可以為每一個調(diào)用創(chuàng)建一個新的對象书蚪,通過前者就可以實現(xiàn)多個進(jìn)程共享一個對象喇澡。關(guān)于這個的用法可以參考Python文檔當(dāng)中的例子“Demonstration of how to create and use customized managers and proxies”。
典型的導(dǎo)出一個共享對象的代碼是:
|
<pre class="python" style="margin: 0px; padding: 0px; background: none; border: none; width: auto; float: none; clear: none; overflow: visible; font-size: 12px; line-height: 1.333; font-family: monospace;">ObjectType object_
class
ObjectManager
(
multiprocessing.
managers
.
BaseManager
)
:
pass
ObjectManager.
register
(
"object"
,
lambda
: object_
)
</pre>
|
注意上面介紹proxy對象的時候殊校,我提到的“公共函數(shù)”四個字晴玖。每個proxy對象只會導(dǎo)出實際對象的公共函數(shù)。這里面有兩個含義为流,一個是“公共”呕屎,即所有非下劃線開頭的成員,另一個是“函數(shù)”敬察,即所有callable的成員秀睛。這就帶來一些限制,一是無法導(dǎo)出屬性莲祸,二是無法導(dǎo)出一些公共的特殊函數(shù)蹂安,例如get, next等等。對于這個mp庫有一套處理锐帜,即自定義proxy對象田盈。首先是BaseManager的register可以提供一個proxy_type作為第三個參數(shù),這個參數(shù)指定了哪些成員需要被導(dǎo)出缴阎。詳細(xì)的使用方法可以參見文檔當(dāng)中的第一個例子允瞧。
另外manager還有一些細(xì)節(jié)的問題需要注意。由于Proxy對象不是線程安全的蛮拔,因此如果需要在一個多線程程序當(dāng)中使用proxy述暂,mp庫會為每個線程創(chuàng)建一個proxy對象,而每個proxy對象都會對server process創(chuàng)建一個連接语泽,而manager那邊對于每個連接都創(chuàng)建一個單獨的線程來為其服務(wù)贸典。這樣帶來的問題就是,如果客戶進(jìn)程有很多線程踱卵,很容易會導(dǎo)致manager進(jìn)程的fd數(shù)目達(dá)到ulimit的限制,即使沒有達(dá)到限制,也會因為manager進(jìn)程當(dāng)中有太多線程而嚴(yán)重影響manager的性能惋砂。解決方案可以是一個進(jìn)程內(nèi)cache妒挎,只有一個單獨的線程可以創(chuàng)建proxy對象訪問共享對象,其余線程只能訪問該進(jìn)程當(dāng)中的cache西饵。
一旦manager因為達(dá)到ulimit限制或者其他異常酝掩,manager會直接退出,遺憾的是眷柔,這時候已經(jīng)建立的proxy會試圖重新連接manager – 但是它已經(jīng)不存在了期虾。這個會導(dǎo)致客戶進(jìn)程hang在對proxy的函數(shù)調(diào)用上,這個時候驯嘱,目前除了殺掉進(jìn)程沒有找到別的辦法镶苞。
另外proxy使用socket的方式比較tricky,因此和內(nèi)置的socket庫有很多沖突鞠评,比如socket.setdefaulttimeout(Python Issue 6056
)茂蚓。在setdefaulttimeout調(diào)用了之后,進(jìn)程當(dāng)中所有通過socket模塊建立的socket都是被設(shè)置為unblock模式的剃幌,但是mp庫并不知道這一點聋涨,而且它總是假設(shè)socket都是block模式的,于是负乡,一旦調(diào)用了setdefaulttimeout牍白,所有對于proxy的函數(shù)調(diào)用都會拋出OSError,錯誤代碼為11抖棘,錯誤原因是非常有誤導(dǎo)性的“Resource temporarily unavailable”淹朋,實際上就是EAGAIN。這個錯誤可以通過我提供的一個patch來補救(這個patch當(dāng)中還包含其他的一些修復(fù)钉答,所以請自行查看并修改該patch)础芍。
由于以上的一些原因,server process模式作為一個對象的共享模式数尿,能夠提供最為靈活的共享方式仑性,但是也有最多的問題。這個在使用過程當(dāng)中就靠自己去衡量了右蹦。目前我們的系統(tǒng)對于數(shù)據(jù)可靠性方面要求不高诊杆,丟失數(shù)據(jù)是可以接受的,但是也只用這種模式來維護(hù)統(tǒng)計值何陆,不敢用來維護(hù)更多的東西晨汹。