Python多进程内存使用

26

我已经编写了一个程序,可以概括如下:

def loadHugeData():
    #load it
    return data

def processHugeData(data, res_queue):
    for item in data:
        #process it
        res_queue.put(result)
    res_queue.put("END")

def writeOutput(outFile, res_queue):
    with open(outFile, 'w') as f
        res=res_queue.get()
        while res!='END':
            f.write(res)
            res=res_queue.get()

res_queue = multiprocessing.Queue()

if __name__ == '__main__':
    data=loadHugeData()
    p = multiprocessing.Process(target=writeOutput, args=(outFile, res_queue))
    p.start()
    processHugeData(data, res_queue)
    p.join()

真实的代码(特别是writeOutput())要复杂得多。writeOutput()只使用它作为参数传递的这些值(这意味着它不引用data)。

基本上,它将一个巨大的数据集加载到内存中进行处理。输出的写入被委托给子进程(它实际上写入多个文件,这需要很长时间)。 因此,每次处理一个数据项时,它都会通过res_queue发送到子进程,后者根据需要将结果写入文件。

子进程不需要以任何方式访问、读取或修改由loadHugeData()加载的数据。子进程只需要使用主进程通过res_queue发送的内容。这就导致了我的问题和疑问。

在使用top检查内存使用情况时,似乎子进程获得了自己的巨大数据集副本。这是真的吗?如果是,那么我该如何避免 id(实质上是使用双倍的内存)?

我正在使用Python 2.6,程序正在运行在Linux系统上。


你能否重构你的代码,使用迭代器来代替一次性加载所有的loadHugeData吗?如果load/process/enqueue/dequeue/write是这样的话,似乎是可以做到的。 - sotapme
“hugeData”很不幸,它是一个基本上包含稀疏数组的制表符分隔文本文件。在处理过程中,我需要根据行号对这些数据进行“随机访问”。因此,将其加载到内存中(使用稀疏数组特定的优化)可以加快处理速度。 - FableBlaze
建议使用像 [beanstalkd](https://github.com/earl/beanstalkc/blob/master/TUTORIAL.mkd) 这样的工具来进行进程集成可能会过度设计,但了解它是否有助于提高性能和扩展性是很有趣的。通常情况下,别人的问题总是更有趣。 - sotapme
2个回答

34
multiprocessing模块实际上是基于fork系统调用的,该调用会创建当前进程的副本。由于在fork(或创建multiprocessing.Process)之前已经加载了大量数据,因此子进程继承了数据的副本。但是,如果您运行的操作系统实现了COW(写时复制),则物理内存中只会有一份数据副本,除非您在父进程或子进程中修改数据(父进程和子进程将共享相同的物理内存页,尽管在不同的虚拟地址空间中)。即使这样,额外的内存也仅对更改分配(以pagesize递增)。您可以在加载大量数据之前调用multiprocessing.Process来避免这种情况发生。那么当您在父进程中加载数据时,子进程将不会反映出这些额外的内存分配。编辑:根据@Janne Karila在答案中的评论,每个Python对象都包含一个引用计数,在访问对象时会修改该计数。因此,仅读取数据结构就可能导致COW复制。

3
比我跑得快做得好。Linux是COW,因此当父进程写入数据时,数据将被复制。如果父进程只读取数据,则数据将只有一个实例,但是top命令(我几乎确定)将显示该数据属于两个进程。使用meminfo应该可以提供更准确的内存使用情况数字。 - Eli Algranti
1
确实。我认为现在最常见的操作系统是COW(我只是尽可能地通用)。这是一个很棒的功能,但在解释基于进程的内存报告工具(例如top、ps等)的输出时经常会引起混淆。Linux上的meminfo和Solaris上的pmap将正确报告,但对于Windows我不清楚 :) - isedev
20
请注意,每个Python对象都包含一个引用计数,在访问对象时会修改该计数。因此,仅读取数据结构就可能导致COW复制。 - Janne Karila
4
谢谢您的回答。在加载数据之前调用multiprocessing.Process似乎解决了问题。我也会查看一下meminfo - FableBlaze
1
@isedev 即使是对表达式进行评估也涉及到临时引用。 - Janne Karila
显示剩余2条评论

2
正文翻译:如文档中推荐的那样(->显式地将资源传递给子进程),我会将大数据(显式地)设为全局变量,这样如果有写时复制(COW)可用且您在新进程中进行分叉(在macOS中默认情况下是生成),则数据可在子进程中使用。
def loadHugeData():
    global data
    return data

def processHugeData(res_queue):
    global data
    for item in data:
        res_queue.put(result)
    res_queue.put("END")

但请记住,Python数据结构是被复制的。由于Python GIL的存在,您需要使用一些更低级的数据类型,例如numpy。

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