Python多进程共享内存与使用参数的比较

8
我正在努力理解在不同进程之间共享相同数据源的最有效和占用内存最少的方法。
想象一下以下代码,它简化了我的问题。
import pandas as pd
import numpy as np
from multiprocessing import Pool

# method #1
def foo(i): return data[i]
if __name__ == '__main__':
    data = pd.Series(np.array(range(100000)))
    pool = Pool(2)
    print pool.map(foo,[10,134,8,1])

# method #2
def foo((data,i)): return data[i]
if __name__ == '__main__':
    data = pd.Series(np.array(range(100000)))
    pool = Pool(2)
    print pool.map(foo,[(data,10),(data,134),(data,8),(data,1)])

在第一种方法中,我们将使用全局变量(仅适用于Linux/OSX,Windows上不起作用),然后通过函数访问它。在第二种方法中,我将"data"作为参数的一部分传递。
在处理过程中使用的内存方面,这两种方法之间会有差异吗?
# method #3
def foo((data,i)): return data[i]
if __name__ == '__main__':
    data = pd.Series(np.array(range(100000)))
    pool = Pool(2)
    # reduce the size of the argument passed
    data1 = data[:1000]
    print pool.map(foo,[(data1,10),(data1,134),(data1,8),(data1,1)])

第三种方法与其传递所有“数据”,因为我们知道只会使用前几条记录,我只传递前1000条记录。这样做会有什么影响吗?
背景:我面临的问题是我有一个大约200万行(内存占用4GB)的大型数据集,将由四个子进程进行一些处理。每个处理过程只涉及少部分数据(20000行),我希望最小化每个并发进程的内存使用。
1个回答

11

我将从第二个和第三个方法开始,因为它们比较容易解释。

当你将参数传递给pool.mappool.apply时,这些参数将被pickle化,使用管道发送到子进程,然后在子进程中进行unpickling。这当然需要你传递的数据结构有两个完全不同的副本。而且,对于大型数据结构,由于pickle/unpickle大对象可能需要相当长的时间,这也可能导致性能较慢。

使用第三种方法,您只需传递比第二种方法更小的数据结构。这应该会表现得更好,因为你不需要pickle/unpickle那么多数据。

还有一点需要注意-多次传递data绝对是个坏主意,因为每个副本都将被反复地pickle/unpickle。你希望将其传递给每个子进程一次。方法1是这样做的好方法,或者你可以使用initializer关键字参数显式地向子进程传递data。这将使用在Linux上使用fork,在Windows上使用pickle来传递数据到子进程:

import pandas as pd
import numpy as np
from multiprocessing import Pool

data = None

def init(_data):
    global data
    data = _data  # data is now accessible in all children, even on Windows

# method #1
def foo(i): return data[i]
if __name__ == '__main__':
    data = pd.Series(np.array(range(100000)))
    pool = Pool(2, initializer=init, initargs=(data,))
    print pool.map(foo,[10,134,8,1])

使用第一种方法,您利用fork的行为来允许子进程继承data对象。fork具有写时复制语义,这意味着在父进程和其子进程之间实际上共享了内存,直到您尝试在子进程中进行写操作。当您尝试进行写操作时,包含要写入数据的内存页必须被复制,以使其与父版本分离。

现在,这听起来就像一个“砰”的一声——只要我们不写入它,就不需要复制任何东西,这肯定比pickle/unpickle方法更快。而且通常情况下确实如此。然而,在实践中,Python在内部编写其对象,即使您真的不期望它这样做。因为Python使用引用计数进行内存管理,所以每次将对象传递给方法或分配给变量等情况时,它都需要增加每个对象的内部引用计数器。因此,传递到子进程的每个对象的内存页包含引用计数的内存页将最终被复制。这肯定比多次pickling data快并且使用更少的内存,但也不完全是共享的。


我遇到了这个错误:`---> 17 print pool.map(foo,[10,134,8,1], intializer=init, initargs=(data,)) TypeError: map() got an unexpected keyword argument 'intializer'`multiprocessing.__version__: '0.70a1' - Primer
1
谢谢 - 解释得非常好,这将满足我的需求! - Alessandro Mariani
@Primer 哎呀,我把 initializer 参数加到了错误的行。它应该在对 Pool(...) 的调用中,而不是对 map 的调用中。我还拼错了 initializer 的拼写。这就是我从记忆中编写代码的后果 :)。无论如何,现在已经修复了。 - dano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接