问题 如何限制多处理过程的范围?


使用python的 multiprocessing 模块,以下设计的示例以最小的内存要求运行:

import multiprocessing 
# completely_unrelated_array = range(2**25)

def foo(x):
    for x in xrange(2**28):pass
    print x**2

P = multiprocessing.Pool()

for x in range(8):
    multiprocessing.Process(target=foo, args=(x,)).start()

取消注释的创建 completely_unrelated_array 并且你会发现每个衍生进程都为该副本分配内存 completely_unrelated_array!这是一个更大的项目的最小例子,我无法弄清楚如何解决;多处理似乎可以复制全局的所有内容。一世  需要共享内存对象,我只需要传入 x,并处理它  整个程序的内存开销。

侧面观察:有趣的是 print id(completely_unrelated_array) 内 foo 给出相同的值,暗示某些可能不是副本......


2687
2017-08-25 00:07


起源

您定位的是哪个版本的Python,以及您使用的是哪个平台? - dano
@dano Python 2.7.6,虽然我有兴趣知道多处理是否在3中发生了变化。 - Hooked
multiprocessing 在Python 3.3中已经改变了很多,引入了 上下文。 - dano
我会说你想把所有子进程中的任何东西都放到他们自己的模块中,对吧?即放 foo 在自己的模块中,然后导入它。 - bbayles
@dano如果你给我看一个修复这个问题的python 3例子,那对我(以及将来可能的其他人)都有用。 - Hooked


答案:


因为性质 os.fork(),你的全局命名空间中的任何变量 __main__ 模块将由子进程继承(假设您在Posix平台上),因此您将看到子进程中的内存使用情况在创建后立即反映出来。我不确定是否所有内存都是真正分配的,据我所知,内存是共享的,直到你真正尝试在子代中更改内存,此时会创建一个新副本。另一方面,Windows不使用 os.fork()  - 它重新导入每个子节点中的主模块,并挑选您想要发送给子节点的任何局部变量。因此,使用Windows,您实际上可以通过仅在内部定义它来避免在子项中复制的大型全局结尾 if __name__ == "__main__": 警卫,因为该守卫内的所有东西都只能在父进程中运行:

import time
import multiprocessing 


def foo(x):
    for x in range(2**28):pass
    print(x**2)

if __name__ == "__main__":
    completely_unrelated_array = list(range(2**25)) # This will only be defined in the parent on Windows
    P = multiprocessing.Pool()

    for x in range(8):
        multiprocessing.Process(target=foo, args=(x,)).start()

现在,在Python 2.x中,您只能创建新的 multiprocessing.Process 如果您正在使用Posix平台,则通过分叉对象。但是在Python 3.4上,您可以使用上下文指定如何创建新进程。所以,我们可以指定 "spawn" 上下文,这是Windows使用的,用于创建我们的新进程,并使用相同的技巧:

# Note that this is Python 3.4+ only
import time
import multiprocessing 

def foo(x):
    for x in range(2**28):pass
    print(x**2)


if __name__ == "__main__":
    completely_unrelated_array = list(range(2**23))  # Again, this only exists in the parent
    ctx = multiprocessing.get_context("spawn") # Use process spawning instead of fork
    P = ctx.Pool()

    for x in range(8):
        ctx.Process(target=foo, args=(x,)).start()

如果您需要2.x支持,或者想要坚持使用 os.fork() 创造新的 Process 对象,我认为你可以做的最好的方法是将报告的内存使用率降低,立即删除子进程中的违规对象:

import time
import multiprocessing 
import gc

def foo(x):
    init()
    for x in range(2**28):pass
    print(x**2)

def init():
    global completely_unrelated_array
    completely_unrelated_array = None
    del completely_unrelated_array
    gc.collect()

if __name__ == "__main__":
    completely_unrelated_array = list(range(2**23))
    P = multiprocessing.Pool(initializer=init)

    for x in range(8):
        multiprocessing.Process(target=foo, args=(x,)).start()
    time.sleep(100)

9
2017-08-25 00:37



谢谢你的回答!如果我理解正确,你的2.x方法仍然会导致副本的开销正确吗?我觉得是时候开始考虑Python 3 ...... - Hooked
@Hooked With os.fork(),除非您写入对象,否则实际上不会在子进程中复制对象。在此之前,内存在分叉子节点的父节点之间共享。但请注意,它 不需要太多 让孩子们写下封面下的继承对象。 - dano
@Hooked你实际上可能会更好 不 试图删除全局变量,因为 global completely_unrelated_array 可能导致对象的INCREF。只是不要在孩子们身上触摸它们。 - dano


这里重要的是您要定位的平台。 Unix系统进程是使用Copy-On-Write(cow)内存创建的。因此,即使每个进程获得父进程的完整内存的副本,该内存实际上仅在每页基础(4KiB)上被修改时进行分配。 因此,如果您只是针对这些平台,则无需进行任何更改。

如果您的目标是没有牛叉的平台,您可能需要使用python 3.4及其新的分叉上下文 spawn 和 forkserver,看到了 文件 这些方法将创建与父进程无共享或有限状态的新进程,并且所有内存传递都是显式的。

但并不是说生成的进程将导入您的模块,因此将显式复制所有全局数据,并且不可能进行写入时复制。为了防止这种情况,您必须减少数据范围。

import multiprocessing  as mp
import numpy as np

def foo(x):
    import time
    time.sleep(60)

if __name__ == "__main__":
    mp.set_start_method('spawn')
    # not global so forks will not have this allocated due to the spawn method
    # if the method would be fork the children would still have this memory allocated
    # but it could be copy-on-write
    completely_unrelated_array = np.ones((5000, 10000))
    P = mp.Pool()
    for x in range(3):
        mp.Process(target=foo, args=(x,)).start()

例如,spawn的顶部输出:

%MEM     TIME+ COMMAND
29.2   0:00.52 python3                                                
0.5   0:00.00 python3    
0.5   0:00.00 python3    
0.5   0:00.00 python3    

和叉子:

%MEM     TIME+ COMMAND
29.2   0:00.52 python3                                                
29.1   0:00.00 python3    
29.1   0:00.00 python3                                                
29.1   0:00.00 python3

请注意,由于写时复制,它超过100%


4
2017-08-25 00:37