我正在使用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}}的数量?
答案 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(或任何其他满足您需要的数量)时,您都可以这样做。如果您的数据集非常大,那么这样做可能会派上用场,而仅靠结果会导致大量内存使用。