使用ThreadPoolExecutor减少内存占用

时间:2018-11-01 15:12:53

标签: python multithreading threadpoolexecutor

我正在使用ThreadPoolExecutor来下载大量(约40万个)关键帧图像。关键帧名称存储在文本文件中(例如 keyframes_list.txt )。

我已经修改了documentation中提供的示例,它似乎可以完美地工作,只有一个例外:显然,该示例将每个链接传递给future对象,所有链接都传递给了一个可迭代对象(准确地说是dict())。此可迭代函数作为参数传递给as_completed()函数,以检查future何时完成。当然,这需要立即将大量文本加载到内存中。我用于此任务的python进程占用1GB的RAM。

完整代码如下所示:

import concurrent.futures
import requests

def download_keyframe(keyframe_name):
    url = 'http://server/to//Keyframes/{}.jpg'.format(keyframe_name)
    r = requests.get(url, allow_redirects=True)
    open('path/to/be/saved/keyframes/{}.jpg'.format(keyframe_name), 'wb').write(r.content)
    return True

keyframes_list_path = '/path/to/keyframes_list.txt'
future_to_url = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
    with open(keyframes_list_path, 'r') as f:
        for i, line in enumerate(f):
            fields = line.split('\t')
            keyframe_name = fields[0]
            future_to_url[executor.submit(download_keyframe, keyframe_name)] = keyframe_name
    for future in concurrent.futures.as_completed(future_to_url):
        keyframe_name = future_to_url[future]
        try:
            future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (keyframe_name, exc))
        else:
            print('Keyframe: {} was downloaded.'.format(keyframe_name))

所以,我的问题是我如何才能提供可迭代的并保持较低的内存占用。我已经考虑过使用queue,但不确定它是否与ThreadPoolExecutor顺利合作。是否有一种简单的方法来控制提交给future的{​​{1}}的数量?

2 个答案:

答案 0 :(得分:1)

如果我们查看source for as_completed(),它所做的第一件事就是使用fs=set(fs)评估您在第221行作为第一个参数传递的所有可迭代项。因此,只要您一次读取并排队整个文件,as_completed()就会在调用它时将所有那些Future实例加载到内存中。

要解决此问题,您需要对输入进行分块,并且每次迭代仅调用带有期货子集的as_completed。您可以使用this answer中的代码段; 〜1k的大块应该使您的线程池保持饱和,同时不消耗过多的内存。您的最终代码(从ThreadPoolExecutor的with块开始)应如下所示:

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
    for lines in grouper(open(keyframes_list_path, 'r'), 1000):
        # reset the dict that as_completed() will check on every iteration
        future_to_url = {}
        for i, line in enumerate(lines):
            fields = line.split('\t')
            keyframe_name = fields[0]
            future_to_url[executor.submit(download_keyframe, keyframe_name)] = keyframe_name
        for future in concurrent.futures.as_completed(future_to_url):
            keyframe_name = future_to_url[future]
            try:
                future.result()
            except Exception as exc:
                print('%r generated an exception: %s' % (keyframe_name, exc))
            else:
                print('Keyframe: {} was downloaded.'.format(keyframe_name))

答案 1 :(得分:1)

AdamKG的答案是一个好的开始,但是他的代码将等到一个块被完全处理后再开始处理下一个块。因此,您会失去一些性能。

我建议采用一种略有不同的方法,该方法将向执行者提供连续的任务流,同时对并行任务的最大数量施加上限,以保持较低的内存占用。

技巧是使用concurrent.futures.wait来跟踪已完成的期货和仍在等待完成的期货:

def download_keyframe(keyframe_name):
    try:
        url = 'http://server/to//Keyframes/{}.jpg'.format(keyframe_name)
        r = requests.get(url, allow_redirects=True)
        open('path/to/be/saved/keyframes/{}.jpg'.format(keyframe_name), 'wb').write(r.content)
    except Exception as e:
        return keyframe_name, e

    return keyframe_name, None

MAX_WORKERS = 8
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
    with open(keyframes_list_path, 'r') as fh:
        futures_notdone = set()
        futures_done = set()
        for i, line in enumerate(fh):
            # Submit new task to executor.
            fields = line.split('\t')
            keyframe_name = fields[0]
            futures_notdone.add(executor.submit(download_keyframe, keyframe_name))

            # Enforce upper bound on number of parallel tasks.
            if len(futures_notdone) >= MAX_WORKERS:
                done, futures_notdone = concurrent.futures.wait(futures_notdone, return_when=concurrent.futures.FIRST_COMPLETED)
                futures_done.update(done)

# Process results.
for future in futures_done:
    keyframe_name, exc = future.result()
    if exc:
        print('%r generated an exception: %s' % (keyframe_name, exc))
    else:
        print('Keyframe: {} was downloaded.'.format(keyframe_name))

当然,您也可以定期在循环内处理结果,以不时清空futures_done。例如,每次futures_done中的项目数超过1000(或任何其他满足您需要的数量)时,您都可以这样做。如果您的数据集非常大,那么这样做可能会派上用场,而仅靠结果会导致大量内存使用。