多处理 - 共享一个复杂的对象

时间:2014-01-06 17:30:33

标签: python concurrency multiprocessing

我有一个类似于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

3 个答案:

答案 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对象,它们也都在使用(共享)相同的“托管”数据对象。