协程池本质是 asyncio.semaphore,用于限制并发数而非复用协程;需在真正消耗资源前(如发起请求时)用 async with semaphore 控制,避免许可证泄漏或内存失控。

协程池本质是 asyncio.Semaphore,不是线程池的翻版
协程本身不占线程,但每个活跃的 async def 函数实例会持有栈帧、局部变量和待恢复的上下文——内存占用随并发数线性增长。所谓“协程池”,实际是用 asyncio.Semaphore 控制同时运行的协程数量,而非复用或回收协程对象。
常见误区是试图用 concurrent.futures.ThreadPoolExecutor 包裹协程,这不仅无效(await 不能在同步线程里直接执行),还会引发 RuntimeWarning: coroutine 'xxx' was never awaited 或死锁。
-
asyncio.Semaphore(n)是唯一轻量、原生、可 await 的限流原语 - 它不管理任务生命周期,只阻塞
acquire()调用,直到有可用许可 - 务必配对使用
async with semaphore:,避免忘记release()导致许可证永久泄漏
正确写法:用 async with semaphore 包裹 awaitable 执行点
关键不是“把任务丢进池”,而是“在真正开始消耗内存前加锁”。比如发起 HTTP 请求、加载大文件、解析 JSON 前一刻才申请许可——而不是在 create_task() 时就锁住。
import asyncio import aiohttp <p>semaphore = asyncio.Semaphore(5) # 最多 5 个并发请求</p><p>async def fetch_url(session, url): async with semaphore: # ← 这里才是控制点 async with session.get(url) as resp: return await resp.text()</p><p>async def main(): async with aiohttp.ClientSession() as session: tasks = [fetch_url(session, url) for url in urls] results = await asyncio.gather(*tasks)</p>
如果把 async with semaphore 放在 create_task() 外层,会导致任务创建被阻塞,队列积压;放在 gather() 外层则完全失效——所有任务已启动。
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
别忽略异常场景下的许可证释放
async with semaphore: 在正常退出时自动释放,但若协程在持有许可期间抛出未捕获异常(如网络超时、JSONDecodeError),且该异常未被上层处理,可能导致许可证卡死——后续任务永远等不到许可。
- 在
async with semaphore:块内做最小必要操作,把可能出错的逻辑尽量收窄 - 必要时手动
try/except并确保semaphore.release()被调用(虽不推荐,但比卡死强) - 更稳妥的做法是用
asyncio.shield()包裹关键段,防止取消传播中断释放流程
内存监控建议:用 sys.getsizeof + task info 辅助验证
asyncio.Task 对象本身很小(几百字节),真正吃内存的是其闭包引用的数据(如下载的响应体、缓存的中间结果)。单纯限制并发数不能解决单任务内存爆炸问题。
验证是否生效,可临时插入检查:
import sys
pending = len(asyncio.all_tasks())
mem_per_task = sum(sys.getsizeof(t) for t in asyncio.all_tasks()) / (pending or 1)
print(f"活跃任务数: {pending}, 平均每任务内存: {mem_per_task:.0f} 字节")
注意:这个值只是估算,真实内存还含 Python 对象引用的间接开销。如果发现 pending 长期远超你设的 Semaphore 值,说明某处漏了 async with 或用了错误的并发原语(比如误用 loop.run_in_executor 启动了同步阻塞操作)。
协程池的边界很薄——它只管“同时跑几个”,不管“每个跑多久”或“每个吃多少内存”。真要稳住内存,得配合流式处理、分块读取、及时 del 大对象,以及用 gc.collect() 在长任务间隙主动触发回收。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










