繁体   English   中英

多处理 - 线程池内存泄漏?

[英]Multiprocessing — Thread Pool Memory Leak?

我正在观察我无法向自己解释的内存使用情况。 下面我提供了我的实际代码的精简版本仍然表现出这种行为。 该代码旨在完成以下任务:

以1000行的块读取文本文件。 每一行都是一个句子。 将这1000个句子分成4个生成器。 将这些生成器传递给线程池,并在250个句子上并行运行特征提取。 在我的实际代码中,我累积了整个文件的所有句子中的特征和标签。 现在出现了一个奇怪的事情:即使没有累积这些值,内存也会被分配但不会被释放! 它与我认为的线程池有关。 总共占用的内存量取决于为任何给定单词提取的特征数量。 我在这里用range(100)模拟这个。 看一看:

from sys import argv
from itertools import chain, islice
from multiprocessing import Pool
from math import ceil


# dummyfied feature extraction function
# the lengt of the range determines howmuch mamory is used up in total,
# eventhough the objects are never stored
def features_from_sentence(sentence):
    return [{'some feature'  'some value'} for i in range(100)], ['some label' for i in range(100)]


# split iterable into generator of generators of length `size`
def chunks(iterable, size=10):
    iterator = iter(iterable)
    for first in iterator:
        yield chain([first], islice(iterator, size - 1))


def features_from_sentence_meta(l):
    return list(map (features_from_sentence, l))


def make_X_and_Y_sets(sentences, i):
    print(f'start: {i}')
    pool = Pool()
    # split sentences into a generator of 4 generators
    sentence_chunks = chunks(sentences, ceil(50000/4))
    # results is a list containing the lists of pairs of X and Y of all chunks
    results = map(lambda x : x[0], pool.map(features_from_sentence_meta, sentence_chunks))
    X, Y = zip(*results)
    print(f'end: {i}')
    return X, Y


# reads file in chunks of `lines_per_chunk` lines
def line_chunks(textfile, lines_per_chunk=1000):
    chunk = []
    i = 0
    with open(textfile, 'r') as textfile:
        for line in textfile:
            if not line.split(): continue
            i+=1
            chunk.append(line.strip())
            if i == lines_per_chunk:
                yield chunk
                i = 0
                chunk = []
        yield chunk

textfile = argv[1]

for i, line_chunk in enumerate(line_chunks(textfile)):
    # stop processing file after 10 chunks to demonstrate
    # that memory stays occupied (check your system monitor)
    if i == 10:
        while True:
            pass
    X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)

我用来调试它的文件有50000个非空行,这就是我在一个地方使用硬编码50000的原因。 如果您想使用相同的文件,他是一个方便的链接:

https://www.dropbox.com/s/v7nxb7vrrjim349/de_wiki_50000_lines?dl=0

现在,当你运行这个脚本并打开你的系统监视器时,你会发现内存已经耗尽,并且使用情况一直持续到第10个块,在那里我人为地进入无限循环以证明内存保持使用,即使我从来没有存储任何东西

你能解释一下为什么会这样吗? 我似乎错过了一些关于如何使用多处理池的东西。

首先,让我们澄清一些误解 - 尽管事实证明,这实际上并不是首先探索的正确途径。

当你在Python中分配内存时,它当然必须从操作系统中获取内存。

但是,当您释放内存时,它很少会返回到操作系统,直到您最终退出。 相反,它进入“免费列表” - 实际上,为不同目的的多个级别的免费列表。 这意味着下次你需要内存时,Python已经有了它,并且可以立即找到它,而无需与操作系统交谈以分配更多内存。 这通常会使内存密集型程序更快。

但这也意味着 - 特别是在现代64位操作系统上 - 通过查看您的活动监视器/任务管理器/等来试图了解您是否确实存在任何内存压力问题。 旁边没用了。


标准库中的tracemalloc模块提供了低级工具,可以查看实际内存使用情况。 在更高的层次上,你可以使用像memory_profiler这样的东西(如果你启用了tracemalloc支持 - 这很重要)可以将这些信息与来自psutil来源的操作系统级信息一起放在一起来弄清楚事情的进展。

但是,如果你没有看到任何实际问题 - 你的系统没有进入交换地狱,你没有得到任何MemoryError异常,你的表现没有达到一些奇怪的悬崖,它会线性扩展到N然后突然在N + 1等地方全力以赴 - 你通常不需要首先打扰任何一个。


如果你确实发现了一个问题,那么,幸运的是,你已经完成了一半的解决方案。 正如我在顶部提到的那样,在您最终退出之前,您分配的大多数内存都不会返回到操作系统。 但是,如果所有内存使用都发生在子进程中,并且这些子进程没有状态,则可以随时退出并重新启动它们。

当然,执行这样的流程拆解和启动时间,以及必须重新开始的页面映射和缓存,并要求操作系统再次分配内存等等都会产生性能成本。 而且还有一个复杂的成本 - 你不能只是运行一个池并让它做它的事情; 你必须参与其中,并为你做回收过程。

执行此操作的multiprocessing.Pool类中没有内置支持。

当然,您可以构建自己的Pool 如果你想获得想象力,你可以看一下multiprocessing的来源并做它的功能。 或者,您可以从Process对象列表和一对Queue构建一个简单的池。 或者您可以直接使用Process对象而无需抽象池。


您可能遇到内存问题的另一个原因是您的个人流程很好,但您只有太多的流程。

事实上,这似乎就是这种情况。

您可以在此函数中创建一个包含4个工作组的Pool

def make_X_and_Y_sets(sentences, i):
    print(f'start: {i}')
    pool = Pool()
    # ...

...并为每个块调用此函数:

for i, line_chunk in enumerate(line_chunks(textfile)):
    # ...
    X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)

因此,每个块最终会有4个新进程。 即使每个人的内存使用率非常低,但同时拥有数百个内存也会增加。

更不用说你可能会因为数百个进程在4个核心上竞争而严重损害你的时间性能,所以你在上下文切换和操作系统调度上浪费时间而不是做实际工作。

正如您在评论中指出的那样,对此的修复是微不足道的:只为每个调用创建一个全局pool而不是新的pool


很抱歉在这里获取所有Columbo,但是......还有一件事......这段代码运行在您模块的顶层:

for i, line_chunk in enumerate(line_chunks(textfile)):
    # ...
    X_chunk, Y_chunk = make_X_and_Y_sets(line_chunk, i)

...那就是试图启动池和所有子任务的代码。 但是该池中的每个子进程都需要import该模块,这意味着它们最终将运行相同的代码,并启动另一个池和一组额外的子任务。

你可能在Linux或macOS上运行它,默认的startmethodfork ,这意味着multiprocessing可以避免这种import ,所以你没有问题。 但是使用其他startmethods,这段代码基本上就是一个吞噬所有系统资源的forkbomb。 这包括spawn ,这是Windows上的默认startmethod。 因此,如果任何人都有可能在Windows上运行此代码,您应该将所有顶级代码放在if __name__ == '__main__': guard中。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM