我有一个大型的 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 不一样。这是否意味着正在复制对象?
附言我认为这不重要,但我使用的是 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
最佳答案
恐怕这里几乎没有任何东西能像您希望那样工作:-(
首先请注意,不同进程产生的相同 id()
值告诉您什么对象是否真的是同一个对象。每个进程都有自己的虚拟地址空间,由操作系统分配。两个进程中的相同虚拟地址可以引用完全不同的物理 内存位置。您的代码是否产生相同的 id()
输出完全是偶然的。在多次运行中,有时我会在您的 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
相应地变得不那么吸引人了。
这里没有简单的答案。不要射杀信使 ;-)
关于python - 多处理 - 共享一个复杂的对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20955683/