asyncio.gather()会撑爆内存,因其将全部协程同时调度进事件循环,为每个协程分配栈帧、闭包和Future对象,导致内存线性增长;实测10k协程可占用500MB+。

为什么 asyncio.gather() 会把内存撑爆?
直接用 asyncio.gather(*[fetch(url) for url in urls]) 处理几千个 URL,本质是把全部 Task 同时调度进事件循环——即便它们还没真正发请求,Python 也会为每个协程对象分配栈帧、闭包和等待状态,加上大量 Future 实例,内存占用呈线性增长。实测 10k 个简单协程可轻易吃掉 500MB+ 内存。
这不是 event loop 的 bug,而是「全量并发」的设计使然:它默认假设你控制了上游并发数(比如已用连接池限流),但多数人只写了协程没设限。
用 asyncio.Semaphore 控制并发数最稳妥
asyncio.Semaphore 是唯一被 asyncio 官方推荐用于限制并发的原语,它不阻塞线程,也不影响事件循环调度,只在 acquire() 时让协程挂起等待空闲许可。
实操要点:
立即学习“Python免费学习笔记(深入)”;
- 初始化时传入期望的并发上限,比如
sem = asyncio.Semaphore(10)表示最多 10 个任务同时执行 - 每个协程开头
async with sem:,离开作用域自动释放,不用手动release() - 务必把
sem实例作为参数传入协程或定义在外部作用域,避免每次新建一个空的Semaphore - 别用
threading.Semaphore或queue.Queue替代——它们不是 awaitable,会阻塞整个 event loop
示例片段:
import asyncio
<p>sem = asyncio.Semaphore(20)</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/skill11526" title="Python Hardener"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20dbae7c507.jpg" alt="Python Hardener" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill11526" title="Python Hardener">Python Hardener</a>
<p>通过完整的安全审查、代码整洁重构、健壮的错误处理、正确的日志记录以及完整的文档字符串覆盖,强化 Python 脚本。</p>
</div>
<a href="/xiazai/skill11526" title="Python Hardener" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>async def fetch_with_limit(url):
async with sem: # 这里会等,直到有 ≤20 个任务在跑
return await aiohttp.get(url)</p><p>tasks = [fetch_with_limit(url) for url in urls]
results = await asyncio.gather(*tasks)
分批 await 比 Semaphore 更省内存?不一定
有人倾向用 for i in range(0, len(urls), batch_size): 切片 + await asyncio.gather(...) 分批执行,看似可控,但隐患明显:
- 每批内部仍是全量并发,比如
batch_size=100,每批仍会瞬时创建 100 个协程对象,内存峰值未必比单次gather低多少 - 批次间有空档,整体吞吐下降,尤其当部分请求极快、部分极慢时,CPU/IO 利用率波动大
- 错误处理麻烦:某一批出错,得重试整批,还是跳过?逻辑易混乱
- 如果批大小设得过大(如 500),依然可能触发内存警戒线
真正适合分批的场景是:下游服务明确要求「每分钟最多 N 次调用」,且你愿意牺牲速度换合规性;否则优先选 Semaphore。
别忽略 aiohttp.ClientSession 的连接池配置
光控协程并发还不够——aiohttp 默认的 TCPConnector 会为每个 ClientSession 创建独立连接池,若 session 复用不当,连接句柄和缓冲区会二次放大内存压力。
关键配置项:
-
limit:单个 session 最大并发连接数(默认 100),应 ≤Semaphore值,否则无意义 -
limit_per_host:对同一 host 的最大连接数(防止单站点打爆),建议设为 3–10 - 始终复用同一个
ClientSession实例,不要在每个协程里async with aiohttp.ClientSession() as session: - 若需不同 timeout / headers,用
session.request(...)参数传,而非新建 session
错误写法:async with aiohttp.ClientSession() as s: ... 出现在协程内部 → 每个任务新建 session → 连接池翻倍 → 内存暴涨。
内存是否真的下来,取决于你有没有同时管住「协程数量」和「连接数量」——Semaphore 控并发,connector 限连接,两者缺一不可。漏掉 connector 配置,就像开着消防栓浇花,光捏住水龙头没用。

















