ere*_*ner 5 python parallel-processing multiprocessing
假设yo = Yo()是一个带有方法的大对象double,它返回其参数乘以2.
如果我通过yo.double到imap的multiprocessing,那么它是非常缓慢的,因为每一个函数调用创建一个副本yo,我认为.
即,这很慢:
from tqdm import tqdm
from multiprocessing import Pool
import numpy as np
class Yo:
def __init__(self):
self.a = np.random.random((10000000, 10))
def double(self, x):
return 2 * x
yo = Yo()
with Pool(4) as p:
for _ in tqdm(p.imap(yo.double, np.arange(1000))):
pass
Run Code Online (Sandbox Code Playgroud)
输出:
0it [00:00, ?it/s]
1it [00:06, 6.54s/it]
2it [00:11, 6.17s/it]
3it [00:16, 5.60s/it]
4it [00:20, 5.13s/it]
Run Code Online (Sandbox Code Playgroud)
...
但是,如果我yo.double用函数包装double_wrap并将其传递给它imap,那么它基本上是瞬时的.
def double_wrap(x):
return yo.double(x)
with Pool(4) as p:
for _ in tqdm(p.imap(double_wrap, np.arange(1000))):
pass
Run Code Online (Sandbox Code Playgroud)
输出:
0it [00:00, ?it/s]
1000it [00:00, 14919.34it/s]
Run Code Online (Sandbox Code Playgroud)
如何以及为什么包装函数会改变行为?
我使用Python 3.6.6.
关于复制,你说得对。yo.double是一个“绑定方法”,绑定到你的大对象。当您将其传递到池方法时,它将用它来pickle整个实例,将其发送到子进程并在那里取消pickle。对于子进程处理的可迭代的每个块都会发生这种情况。默认值chunksizeinpool.imap1,因此对于可迭代中的每个已处理项,您都会遇到此通信开销。
相反,当您传递 时double_wrap,您只是传递一个模块级函数。只有它的名称实际上会被腌制,并且子进程将从 导入该函数__main__。由于您显然使用的是支持分叉的操作系统,因此您的double_wrap函数将可以访问分叉yo的实例Yo. 在这种情况下,您的大对象不会被序列化(腌制),因此与其他方法相比,通信开销很小。
\n\n\n@Darkonaut 我只是不明白为什么设置功能模块级别会阻止对象的复制。毕竟,该函数需要有一个指向 yo 对象本身 \xe2\x80\x93 的指针,这应该要求所有进程复制 yo,因为它们无法共享内存。
\n
在子进程中运行的函数将自动查找对全局的引用yo,因为您的操作系统(OS)正在使用 fork 来创建子进程。分叉会导致整个父进程的克隆,只要父进程和子进程都没有更改特定对象,两者都会在同一内存位置看到相同的对象。
仅当父进程或子进程更改了对象上的某些内容时,该对象才会在子进程中复制。这就是所谓的“写时复制”,发生在操作系统级别,而您在 Python 中却没有注意到它。您的代码无法在 Windows 上运行,它使用“spawn”作为新进程的启动方法。
\n\n现在我在上面写的“对象被复制”的地方进行了一些简化,因为操作系统操作的单元是一个“页面”(最常见的大小是 4KB)。这个答案在这里将是一个很好的后续阅读,可以扩大您的理解。
\n