我有一个类似于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 :(得分:14)
我担心这里几乎没有任何东西按照你希望的方式运作: - (
首先请注意,不同进程生成的相同id()
值告诉您 nothing 关于对象是否真的是同一个对象。每个进程都有自己的虚拟地址空间,由操作系统分配。两个进程中的相同虚拟地址可以指代完全不同的物理内存位置。您的代码是否产生相同的id()
输出几乎是偶然的。在多次运行中,有时我会在id()
部分中看到不同的Process
输出,并在id()
部分中重复Pool
输出,反之亦然,或两者兼而有之。
其次,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
成员,因为所有{{1}方法在单个进程中运行(管理器进程)。这就是列表ID始终相同的原因。
如果您在Linux-y系统(支持numeri
的操作系统)上运行,更好的办法是忘记所有这些fork()
内容并在模块上创建复杂对象启动任何工作进程之前的级别。然后工作人员将继承复杂对象的(地址空间副本)。通常的写时复制Manager
语义将使内存效率尽可能高。如果突变不需要折叠回主程序复杂对象的副本,那就足够了。如果确实需要折叠突变,那么您需要重新进行大量的进程间通信,fork()
相应地变得不那么有吸引力了。
这里没有简单的答案。不要射击信使; - )
答案 1 :(得分: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之间来回切换。
目前的答案来自于__class__
期间实际打印produce
属性。每次运行,__class__
实际上都是
<class 'multiprocessing.managers.AutoProxy[numeri]'>
因此numeri
对象每次都包含在AutoProxy
中,而AutoProxy
并不总是相同。但是,每次调用numeri
时,被包装的produce
对象都是相同的。如果您在appendi
中拨打produce
方法一次,那么listaNumeri
最终将在您的计划结束时提供10个项目。
答案 2 :(得分: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
如图所示,即使每个Manager
进程都有一个不同的Pool
对象,它们也都在使用(共享)相同的“托管”数据对象。