2014-01-06 53 views
11

我有一個很大的dict類似的對象,需要在衆多工作進程之間共享。每個工作人員讀取對象中信息的隨機子集並對其進行一些計算。我想避免複製大型對象,因爲我的機器快速耗盡內存。多處理 - 共享一個複雜對象

我在玩this SO question的代碼,我修改了一下以便使用固定大小的進程池,這更適合我的用例。然而這似乎打破了它。

from multiprocessing import Process, Pool 
from multiprocessing.managers import BaseManager 

class numeri(object): 
    def __init__(self): 
     self.nl = [] 

    def getLen(self): 
     return len(self.nl) 

    def stampa(self): 
     print self.nl 

    def appendi(self, x): 
     self.nl.append(x) 

    def svuota(self): 
     for i in range(len(self.nl)): 
      del self.nl[0] 

class numManager(BaseManager): 
    pass 

def produce(listaNumeri): 
    print 'producing', id(listaNumeri) 
    return id(listaNumeri) 

def main(): 
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi', 
         'svuota', 'stampa']) 
    mymanager = numManager() 
    mymanager.start() 
    listaNumeri = mymanager.numeri() 
    print id(listaNumeri) 

    print '------------ Process' 
    for i in range(5): 
     producer = Process(target=produce, args=(listaNumeri,)) 
     producer.start() 
     producer.join() 

    print '--------------- Pool' 
    pool = Pool(processes=1) 
    for i in range(5): 
     pool.apply_async(produce, args=(listaNumeri,)).get() 

if __name__ == '__main__': 
    main() 

輸出是

4315705168 
------------ Process 
producing 4315705168 
producing 4315705168 
producing 4315705168 
producing 4315705168 
producing 4315705168 
--------------- Pool 
producing 4299771152 
producing 4315861712 
producing 4299771152 
producing 4315861712 
producing 4299771152 

正如你所看到的,在第一種情況下的所有工作進程得到相同的對象(通過ID)。在第二種情況下,id不一樣。這是否意味着對象被複制?

P.S.我不認爲事情,但我使用joblib,其內部使用的一個Pool

from joblib import delayed, Parallel 

print '------------- Joblib' 
     Parallel(n_jobs=4)(delayed(produce)(listaNumeri) for i in range(5)) 

,輸出:

------------- Joblib 
producing 4315862096 
producing 4315862288 
producing 4315862480 
producing 4315862672 
producing 4315862352 
+0

檢查http://zeromq.org/,它使得以簡單的方式在進程之間共享信息。 – clopez

回答

13

恐怕幾乎沒有在這裏的工作方式,你希望它的工作原理:-(

首先注意到相同id()值所產生不同進程告訴你什麼都不的對象是否真的每個進程都有自己的虛擬地址空間,由操作系統分配,兩個進程中相同的虛擬地址可以參考完全不同的物理地址內存位置,不管你的代碼是否產生相同的輸出, PUR意外的。在多次運行中,有時我會在您的Process部分中看到不同的id()輸出,並在您的Pool部分中反覆輸出id()輸出,反之亦然,或兩者兼而有之。

其次,Manager用品語義共享而不是物理共享。您的numeri實例的數據在管理器進程中僅在處生效。您的所有工作進程都會查看(副本)代理對象。這些都是很薄的包裝,可以將所有操作轉交給經理進程執行。這涉及到很多進程間通信,以及管理器進程內部的序列化。這是編寫速度非常慢的代碼的好方法;-)是的,只有一個numeri數據副本,但所有工作都是由單個進程完成的(管理進程)。

爲了更清楚地看到這一點,進行更改@martineau建議,也改變get_list_id()這樣:

def get_list_id(self): # added method 
    import os 
    print("get_list_id() running in process", os.getpid()) 
    return id(self.nl) 

這裏的示例輸出:

41543664 
------------ Process 
producing 42262032 
get_list_id() running in process 5856 
with list_id 44544608 
producing 46268496 
get_list_id() running in process 5856 
with list_id 44544608 
producing 42262032 
get_list_id() running in process 5856 
with list_id 44544608 
producing 44153904 
get_list_id() running in process 5856 
with list_id 44544608 
producing 42262032 
get_list_id() running in process 5856 
with list_id 44544608 
--------------- Pool 
producing 41639248 
get_list_id() running in process 5856 
with list_id 44544608 
producing 41777200 
get_list_id() running in process 5856 
with list_id 44544608 
producing 41776816 
get_list_id() running in process 5856 
with list_id 44544608 
producing 41777168 
get_list_id() running in process 5856 
with list_id 44544608 
producing 41777136 
get_list_id() running in process 5856 
with list_id 44544608 

清除?究其原因,你會得到相同的列表ID每次都是因爲具有相同self.nl成員的每個工作進程,這是因爲在所有numeri方法運行一個進程(經理處理)。這就是爲什麼列表ID始終相同。

如果您在Linux-y系統(支持fork()的操作系統)上運行,更好的辦法是在啓動任何工作進程之前忘記所有這些Manager的東西,並在模塊級別創建複雜的對象。然後這些工作人員將繼承您複雜對象的(地址空間副本)。通常的寫時複製fork()語義將盡可能提高內存的效率。如果突變不需要折回到主程序的複雜對象副本中,這就足夠了。如果突變需要折回,那麼你又回到了需要大量進程間通信的地步,並且multiprocessing相應地變得不那麼有吸引力。

這裏沒有簡單的答案。不要拍攝信使;-)

+0

謝謝,所有三個答案都很好,我希望我能接受他們所有人。我認爲這是一種編寫慢代碼的好方法,我只是試過了。我的應用程序目前是內存綁定的,因爲我沒有足夠的RAM來運行儘可能多的內核。我希望通過減少內存使用量,我可以讓更多的工作人員運行起來。事實的確如此,除了工人變慢三倍。 – mbatchkarov

+0

我必須同意這是最好的答案 - 但我期望從Mr ['import this'](http://stackoverflow.com/questions/228181/the-zen-of-python)Peters不會更少。 ;-) – martineau

+0

@mbatchkarov,而'經理'聽起來不適合你的問題,還有很多其他的方式來使用'多處理'。也許你可以用適當的可執行代碼打開另一個描述問題的問題。例如,也許主程序可以選擇「隨機樣本」,並只傳遞給工作人員,而不是傳遞整個對象。但更具體的答案需要更具體的問題;-) –

4

如果您添加兩行代碼,你會發現一些關於這種行爲很怪異:

def produce(listaNumeri): 
    print 'producing', id(listaNumeri) 
    print listaNumeri # <- New line 
    return id(listaNumeri) 


def main(): 
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi', 'svuota', 'stampa', 'getAll']) 
    mymanager = numManager() 
    mymanager.start() 
    listaNumeri = mymanager.numeri() 
    print listaNumeri # <- New line 
    print id(listaNumeri) 

這給你以下的輸出:

<__main__.numeri object at 0x103892990> 
4354247888 
------------ Process 
producing 4354247888 
<__main__.numeri object at 0x103892990> 
producing 4354247888 
<__main__.numeri object at 0x103892990> 
producing 4354247888 
<__main__.numeri object at 0x103892990> 
producing 4354247888 
<__main__.numeri object at 0x103892990> 
producing 4354247888 
<__main__.numeri object at 0x103892990> 
--------------- Pool 
producing 4352988560 
<__main__.numeri object at 0x103892990> 
producing 4354547664 
<__main__.numeri object at 0x103892990> 
producing 4352988560 
<__main__.numeri object at 0x103892990> 
producing 4354547664 
<__main__.numeri object at 0x103892990> 
producing 4352988560 
<__main__.numeri object at 0x103892990> 

正如你所看到的,對象每次都是相同的但是id並不總是相同的。此外,請查看池部分中使用的ID - 它在兩個ID之間來回切換。

問題的答案是在produce期間實際打印__class__屬性。每次運行中,__class__每次實際

<class 'multiprocessing.managers.AutoProxy[numeri]'> 

所以numeri對象被包裹在一個AutoProxy,而AutoProxy並不總是相同的。但是,每次調用produce時,被封裝的對象numeri都是相同的。如果您在produce中調用appendi方法一次,那麼listaNumeri最終將在程序結束時顯示10個項目。

4

您正在混淆對象實例numeri及其管理器listaNumeri。這可以通過使一些小的修改代碼來說明:

首先添加get_list_id方法class numeri(object)返回實際內部數據結構的id被使用:

...             
    def get_list_id(self): # added method 
     return id(self.nl) 

然後修改produce()使用它:

def produce(listaNumeri): 
    print 'producing', id(listaNumeri) 
    print ' with list_id', listaNumeri.get_list_id() # added 
    return id(listaNumeri) 

最後,一定要公開新方法爲numManager界面的一部分:

def main(): 
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi', 
                'svuota', 'stampa', 
                'get_list_id']) # added 
    ...             

之後,您將看到類似以下的輸出:

13195568 
------------ Process 
producing 12739600 
with list_id 13607080 
producing 12739600 
with list_id 13607080 
producing 12739600 
with list_id 13607080 
producing 12739600 
with list_id 13607080 
producing 12739600 
with list_id 13607080 
--------------- Pool 
producing 13690384 
with list_id 13607080 
producing 13691920 
with list_id 13607080 
producing 13691888 
with list_id 13607080 
producing 13691856 
with list_id 13607080 
producing 13691824 
with list_id 13607080 

由於這表明,即使有,他們都使用(共享)相同的是每個Pool過程不同Manager對象「受管理」的數據對象。