Python 多处理-共享复杂对象

Python 多处理-共享复杂对象,python,concurrency,multiprocessing,Python,Concurrency,Multiprocessing,我有一个类似dict的大型对象,需要在多个工作进程之间共享。每个worker读取对象中的一个随机信息子集,并使用它进行一些计算。我想避免复制大对象,因为我的机器内存很快就用完了 我正在使用的代码,并对其进行了一些修改,以使用固定大小的进程池,这更适合我的用例。然而,这似乎打破了它 from multiprocessing import Process, Pool from multiprocessing.managers import BaseManager class numeri(obje

我有一个类似dict的大型对象,需要在多个工作进程之间共享。每个worker读取对象中的一个随机信息子集,并使用它进行一些计算。我想避免复制大对象,因为我的机器内存很快就用完了

我正在使用的代码,并对其进行了一些修改,以使用固定大小的进程池,这更适合我的用例。然而,这似乎打破了它

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
------------过程
生产4315705168
生产4315705168
生产4315705168
生产4315705168
生产4315705168
---------------水池
生产4299771152
生产4315861712
生产4299771152
生产4315861712
生产4299771152
如您所见,在第一种情况下,所有工作进程都获得相同的对象(按id)。在第二种情况下,id不同。这是否意味着正在复制对象

另外,我认为这并不重要,但我使用的是
joblib
,它在内部使用了

from joblib import delayed, Parallel

print '------------- Joblib'
        Parallel(n_jobs=4)(delayed(produce)(listaNumeri) for i in range(5))
哪些产出:

-----------Joblib
生产4315862096
生产4315862288
生产4315862480
生产4315862672
生产4315862352

如果在代码中添加两行,您会发现这种行为非常奇怪:

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)

因此,
numeri
对象每次都被包装在一个
AutoProxy
中,而
AutoProxy
并不总是相同的。但是,在每次调用
product
时,要包装的
numeri
对象是相同的。如果在
product
中调用
appendi
方法一次,则
listaNumeri
将在程序的末尾包含10项。

您将对象实例
numeri
与其管理器
listaNumeri
混淆。这可以通过对代码进行一些小的修改来说明:

首先将
get\u list\u id
方法添加到
class numeri(object)
中,该方法返回所使用的实际内部数据结构的
id

    ...                                                   
    def get_list_id(self):  # added method
        return id(self.nl)
然后修改
product()
以使用它:

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
    ...                                                   
之后,您将看到类似以下输出的内容:

<__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>
13195568
------------过程
生产12739600
列表号为13607080
生产12739600
列表号为13607080
生产12739600
列表号为13607080
生产12739600
列表号为13607080
生产12739600
列表号为13607080
---------------水池
生产13690384
列表号为13607080
生产13691920
列表号为13607080
生产13691888
列表号为13607080
生产13691856
列表号为13607080
生产13691824
列表号为13607080

如图所示,尽管每个
进程都有不同的
管理器
对象,但它们都在使用(共享)相同的“托管”数据对象。

恐怕这里没有任何东西能像您希望的那样工作:-(

首先请注意,相同的
id()
不同进程生成的值无法告诉您对象是否真的是相同的对象。每个进程都有自己的虚拟地址空间,由操作系统分配。两个进程中相同的虚拟地址可以引用完全不同的物理内存位置。您的代码是否生成相同的
id()
输出与否完全是偶然的。在多次运行中,有时我会在
进程
部分看到不同的
id()
输出,并在
部分重复
id()
输出,反之亦然,或者两者都重复

其次,
Manager
提供语义共享,但不提供物理共享。您的
numeri
实例的数据仅存在于Manager进程中。您的所有工作进程都可以查看(的副本)代理对象。这些是转发要由管理器进程执行的所有操作的精简包装。这涉及到大量进程间通信和管理器进程内的序列化。这是编写速度非常慢的代码的一种好方法;-)是的,只有一个
numeri
数据的副本,但它的所有工作都是由一个过程(经理过程)完成的

要更清楚地看到这一点,请进行@martineau建议的更改,并将
get\u list\u 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()
语义将尽可能提高内存效率。如果不需要将突变折叠回主程序的复杂对象副本中,这就足够了。如果突变确实需要重新折叠,那么您就需要大量的进程间通信,
多处理
相应地变得不那么吸引人


这里没有简单的答案。不要向信使开枪;-)

检查,它以一种简单的方式在进程之间共享信息。谢谢,这三个答案都很好,我希望我能接受它们。我理解你的观点,这是一种编写慢代码的好方法,我只是尝试了一下。我的应用程序目前内存有限,因为我没有足够的RAM来运行像内核一样多的工作线程。我希望
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