ThreadPoolExecutor和ProcessPoolExecutor默认爆内存主因是任务提交过快导致队列无节制堆积,而非并发数过多;需用max_workers配合as_completed实时消费结果,并控制提交节奏。

为什么 ThreadPoolExecutor 和 ProcessPoolExecutor 默认会爆内存?
不是线程/进程开多了才爆,而是任务提交太快、队列无节制堆积导致的。比如用 submit() 循环扔 10 万个耗时短但返回大对象的任务,Future 对象本身不占多少内存,但所有待执行或已完成但未 result() 取回的结果,全堆在内部队列和引用里——尤其 ProcessPoolExecutor 还要序列化数据进出子进程,内存翻倍增长很常见。
用 max_workers + as_completed 控制实时并发数
单纯设小 max_workers 不够,必须配合结果消费节奏,否则提交仍在狂塞。
-
max_workers设为 CPU 核心数(CPU 密集)或略高(IO 密集),避免无谓竞争 - 别用
submit()+ 列表存所有Future,改用as_completed()边跑边取结果,释放已完成任务的内存引用 - 如果任务返回值很大(如 pandas DataFrame),在
as_completed循环里立刻处理或丢弃,别 accumulate 到列表
from concurrent.futures import ThreadPoolExecutor, as_completed
<p>def fetch_data(url):</p><h1>返回一个几 MB 的 dict 或 DataFrame</h1><pre class="brush:php;toolbar:false;">return {...}urls = [f"https://www.php.cn/link/2bb054c14409adcb28cb8d922e10a383}" for i in range(10000)]
立即学习“Python免费学习笔记(深入)”;
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
with ThreadPoolExecutor(max_workers=8) as executor:
提交但不攒 Future 列表
future_to_url = {executor.submit(fetch_data, u): u for u in urls[:100]} # 先扔前 100 个
remaining = urls[100:]
for future in as_completed(future_to_url):
try:
data = future.result() # 立即取回、立即用/丢
process(data) # 关键:这里处理完就让 data 被 gc
except Exception as e:
pass
finally:
# 补一个新任务,保持池中始终有约 100 个 pending/submitted
if remaining:
u = remaining.pop(0)
future_to_url[executor.submit(fetch_data, u)] = u
用 queue_size(需自定义)堵住提交端口
concurrent.futures 原生不提供队列长度限制,但可通过包装 ThreadPoolExecutor 实现“提交阻塞”:当待执行任务数超阈值,submit() 就卡住,倒逼上游降速。
- 核心是用
queue.Queue(maxsize=N)包一层,把submit改成往队列 put,worker 线程从队列 get 后再真实 submit - 选
N ≈ max_workers × 2比较稳妥,太小易饿死,太大仍可能积压 - 注意:这会增加一层调度延迟,对超低延迟场景慎用;且
ProcessPoolExecutor不适合套 queue,因进程启动开销大,不如直接控max_workers+as_completed
为什么 wait() 和 shutdown(wait=True) 容易引发假死或 OOM?
这两个操作会等所有已提交任务结束才返回,但如果你之前已经疯狂 submit() 了 5 万个任务,它们还在排队或执行中,wait() 就得一直 hold 住,期间所有中间结果、闭包变量、线程栈全吊着不释放。
- 永远不要在大批量提交后直接调
executor.shutdown(wait=True) - 改用
as_completed或map()(它内部自动节流)来分批驱动 - 若必须等全部完成,确保提交总数可控,或提前用
future.cancel()主动清理掉不需要的 pending 任务
真正难控的从来不是并发数,而是「任务生成速度」和「结果消费速度」之间的差值——这个差值决定了内存到底堆在哪一层:是线程栈、结果队列、还是你自己的 list 缓存里。

















