Python 多处理创建新的内存位置

Python multiprocessing creates new memory location

我在使用 python 的 multiprocessing 软件包时遇到问题。我试图通过以下示例尽可能地简化事情。我们有 2 个记忆库。每个都有自己的 "separate" 内存 (mem),这是一个 dict。他们还可以访问共享内存 (shared_mem),这是一个 mp.Array。经过计算,mem填满了3个entry,shared_mem应该等于两个单独的mem的entry-wise乘积。我们期望每个 mem{0: 0, 1: 1, 2: 2}shared_mem[0, 1, 4].

import multiprocessing as mp

class MemBank(object):
    def __init__(self, shared_mem):
        self.mem = dict()
        self.shared_mem = shared_mem

    def fill_mem(self, n):
        print "\tfilling: id, shared_id = %s %s" % (id(self.mem), id(self.shared_mem))
        for i in xrange(n):
            self.mem[i] = i
            self.shared_mem[i] *= self.mem[i]
        print "\tmem = "+str(self.mem)
        print "\tshared_mem = "+str([elt for elt in self.shared_mem])

if __name__ == "__main__":

    P = 2
    n = 3

    # initialize memory banks
    mem_bank = dict()
    shared_mem = mp.Array('f', [1,1,1], lock=True)
    print "shared_id =", id(shared_mem)
    for p in xrange(P):
        mem_bank[p] = MemBank(shared_mem)
        print "p, id, shared_id =", p, id(mem_bank[p].mem), id(mem_bank[p].shared_mem)

    # fill memory banks in parallel
    processes = [mp.Process(target=mem_bank[p].fill_mem, args=(3,)) for p in xrange(P)]
    for process in processes:
        process.start()
    for process in processes:
        process.join()

    # view the results
    for p in xrange(P):
        bank_p = mem_bank[p]
        print "p, id, shared_id =", p, id(bank_p.mem), id(bank_p.shared_mem)
        print "\tmem =", bank_p.mem
        print "\tshared_mem =", [elt for elt in bank_p.shared_mem]

我正在使用 windows,上面的代码位于 python 包的一个模块中。为了 运行 它,我从命令行导航到包目录,然后执行 python -m path.to.module。结果:

C:\path\to\package>python -m package.path.to.module
shared_id = 39625840
p, id, shared_id = 0 39624544 39625840
p, id, shared_id = 1 39649328 39625840
        filling: id, shared_id = 38894304 39278672
        mem = {0: 0, 1: 1, 2: 2}
        shared_mem = [0.0, 1.0, 2.0]
        filling: id, shared_id = 39942016 40319056
        mem = {0: 0, 1: 1, 2: 2}
        shared_mem = [0.0, 1.0, 4.0]
p, id, shared_id = 0 39624544 39625840
        mem = {}        
        shared_mem = [0.0, 1.0, 4.0]
p, id, shared_id = 1 39649328 39625840
        mem = {}        
        shared_mem = [0.0, 1.0, 4.0]

我的问题:正如我们从打印输出中看到的那样,我填充了两个 mem 并使用 multiprocessing 并行计算 shared_mem(这是 fill_mem方法)。一切正常,直到我尝试查看 mems post 计算。它们显示为空,即使它们在计算期间被填充并且 shared_mem 具有正确的结果。 mem 属性不需要共享,我想在不共享它们的情况下恢复它们 post 计算。

Everything's fine until I try to view the mems post computation. They show up as empty, even though they were filled during the computation ...

啊,但是他们不是

multiprocessing想成是把自己1复制给其他人的能力,然后指挥near-clones现在独立的人去做事物。我们从 Jon(原始文件)开始并制作两个副本:Jon0 (processes[0]) 和 Jon1。我们还给每个克隆一个 args= 的副本(不是原件)和 target= 的一个副本(不是原件),尽管通常这并不重要。2

Jon0 跑掉了3 并用一些私有数据(没有其他人看到)和共享的东西做了一些事情。每当他想读取或写入共享的东西时,如有必要,他会排队等待锁,获取锁,执行 read-or-write,然后释放锁。当他完成后,他消失在一团烟雾中,只留下一个退出代码。4

Jon1 同样熄灭并做了一些事情(几乎是同一件事),最终也消失在一团烟雾中。

最初的 "us",又名 Jon,现在正等待 Jon0 和 Jon1 发声。然后我们继续打印 mem_bank[0]mem_bank[1]。它们完全没有变化,这本身并不奇怪。令人惊讶的 mem_bank[0].mem[key]mem_bank[1].mem[key] 也没有变化......但现在不那么令人惊讶了,因为我们给了 Jon0 一个 copy 我们的空字典 mem_bank[0].mem。他修改了他的副本,而不是我们的原件。同样,Jon1 修改了他的 mem_bank[1].mem 副本,而不是我们的原始副本。

我们唯一看到任何变化的地方是 shared_mem,因为它具有特殊的共享类型和关联锁。 shared_mem mp.Array object 上的操作进入共享保险库,伴随着花哨的 "wait for the lock, take the lock, go into the shared vault and do the thing, and then put back the lock" 舞蹈。这包括我们(原来的 Jon)对共享 object 的自己的 阅读,即使在 Jon0 和 Jon1 搞砸之后也是如此。当然,既然没有其他人拿锁,我们总是会立即获得它,但访问这些东西仍然需要 time-cost。

请注意 multiprocessingthreading 提供类似的创建和连接方法。它们之间的一个关键区别是线程根本不会创建 totally-independent 克隆,而是一种 conjoined-twin:一个单独的大脑居住在同一个 body 中。 (唉,由于 cpython 全局解释器锁,许多看似独立的操作 可以 在多个 CPU 上并行完成 single-threaded 尽管 "threading"。)其余差异大多存在,因为一旦您拥有独立的克隆,您就会发现他们需要共享通信渠道,以便他们可以相互交谈。连体双胞胎,共享一个 body,不需要那个:一个人可以把东西放在手上然后睡觉,唤醒另一个大脑,然后可以看着它的手。


1WindowsPython和Linux/UnixPython之间的主要区别在于copy-to-clone是如何发生的.在 Unix-like 系统上,克隆通过 os.fork 发生,复制 "you" 目前拥有的任何东西:此时你知道的所有东西 also 得到复制。然而,在 Windows 上,克隆是通过生成 Python、运行 相同程序的新(空)实例,但在某处 运行 开始复制 5 在多处理代码中,__name__ 而不是 '__main__'

2复制是通过pickle模块完成的,将Pythonclasses和数据objects转换为字符串。当 pickle 失败时,复制过程很重要!关于字符串如何与克隆交换的细节通常完全无关紧要,并且在 Unix 和 Windows.

上有所不同

3这 "running off" 发生在您调用 start 时,或之后不久。克隆本身直到那时才被创建,一旦创建,它就会读入 pickled targetargskwargs。然后它调用self.run,其整个代码如下:

    if self._target:
        self._target(*self._args, **self._kwargs)

这意味着您可以创建一个不使用 self._target 和两个参数的 Process 的 sub-class,让您不必费心传递任何参数。没有什么实际意义尽管如此:这样做只是为了与 Thread class.

保持对称

4退出代码通过操作系统传回,因此仅限于 OS 提供的内容。

5Windows 上的管理方式特别棘手。本质上,原始进程生成一个新的 Python 命令,带有 command-line 个参数:

    if getattr(sys, 'frozen', False):
        return [sys.executable, '--multiprocessing-fork']
    else:
        prog = 'from multiprocessing.forking import main; main()'
        opts = util._args_from_interpreter_flags()
        return [_python_exe] + opts + ['-c', prog, '--multiprocessing-fork']

这种复杂的方法允许 multiprocessing 模块(至少在大多数情况下)检测未能使用正确编程习惯用法的代码(既防止无限递归,又调用特殊的多处理 freeze-support 代码在 __main__ 部分(如果需要))。

在 Unix-like 系统上要容易得多,其中 multiprocessing 可以调用 os.fork 来制作一个即将从 [=28= return ] 称呼。新的克隆人知道他是克隆人,因为 his os.fork 调用 returns 0,而原始人知道他是原始人,因为 他的 os.fork 调用 returns 克隆的 ID。