假设yo = Yo()
是一个带有方法double
的大对象,该方法返回其参数乘以2
。
如果我将yo.double
传递给imap
的{{1}},那么它会非常慢,因为我认为每个函数调用都会创建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
...
但是,如果我用函数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]
包装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
包装函数如何以及为何改变行为?
我使用Python 3.6.6。
答案 0 :(得分:1)
您对复制是正确的。 yo.double
是绑定到您的大对象的“绑定方法”。当您将其传递到池方法中时,它将使用它来腌制整个实例,将其发送到子进程并在那里进行修补。对于子进程所处理的每个可迭代块,都会发生这种情况。 chunksize
中pool.imap
的默认值为1,因此您要为Iterable中的每个已处理项目节省通信开销。
相反,当您传递double_wrap
时,您只是在传递模块级函数。实际上,只有它的名称会被腌制,并且子进程将从__main__
导入函数。由于您显然是在支持分叉的操作系统上,因此您的double_wrap
函数将有权访问yo
的分叉的Yo
实例。在这种情况下,您的大对象将不会被序列化(插入),因此与其他方法相比,通信开销很小。
@Darkonaut我只是不明白为什么将功能模块设置为水平会阻止对象的复制。毕竟,该函数需要有一个指向yo对象本身的指针-这应该要求所有进程都复制yo,因为它们无法共享内存。
在子进程中运行的函数将自动找到对全局yo
的引用,因为您的操作系统(OS)正在使用fork创建子进程。分叉会导致整个父进程的克隆,只要父进程和子进程都没有更改特定的对象,那么两者都将在相同的内存位置看到相同的对象。
仅当父级或子级更改对象上的某些内容时,对象的对象才会在子进程中被复制。这就是所谓的“写时复制”,发生在操作系统级别,而您没有在Python中注意到它。您的代码在Windows上无法使用,Windows使用“ spawn”作为新进程的启动方法。
现在,我在上面写“对象被复制”的地方进行了简化,因为操作系统所操作的单元是一个“页面”(最常见的大小是4KB)。这个答案here是扩大您的理解的很好的后续阅读。