直接查看len(asyncio.all_tasks()),若远超并发预期(如50并发却达300+),基本可确认是Task堆积;未捕获异常会导致Task静默挂起,done()为False但exception()非空;cancel()仅设标志,需协程体真正退出帧栈才能释放内存。

Python 异步程序内存不降、RSS 持续上涨,八成是 asyncio.Task 没被真正清理,而不是协程写错了或 GC 失效。
怎么快速确认是不是 Task 堆积导致的泄漏
别猜,直接看事件循环里还活着什么:
- 调用
len(asyncio.all_tasks()),如果数值远超并发预期(比如 HTTP 服务平均 50 并发,但返回 300+),基本就是它 - 加个健康接口输出最老的几个 Task:
t.get_coro().__name__和t.get_coro().cr_frame.f_lineno,能立刻定位到哪段代码在“甩手不管” - 注意:未捕获异常会让 Task 静默挂起——
t.done()返回False,但t.exception()非空,这种 Task 既不报错也不退出,专吃内存
为什么 task.cancel() 后内存还是不下来
因为 cancel 只是设 flag,真正释放要靠协程体自己退出帧栈。常见卡点:
- 协程卡在
await asyncio.Lock.acquire()或await queue.get()这类等待点,CancelledError被压住没抛出 - 用了阻塞调用如
time.sleep(),事件循环无法注入取消信号 - cancel 后没
await task或asyncio.gather(..., return_exceptions=True),Task 一直停在CANCELLED状态,_coro.cr_frame和_traceback全部锁死 - 紧急修复可用私有属性清理:
task._exception = None、task._traceback = None(仅限调试,生产慎用)
如何避免协程对象和 Task 被隐式强持有
很多泄漏不是你忘了 await,而是对象被悄悄“焊死”在某个地方:
立即学习“Python免费学习笔记(深入)”;
- 日志
extra字典里传了task实例或闭包变量,logging handler 持有引用不放 - 全局容器(如
dict、list)存了 Task 或协程对象,没配 TTL 或清理逻辑 - 第三方库内部缓存了 Task 或连接句柄(如旧版
aiofiles、某些 ORM 的异步 session),升级前先查 changelog - 用
weakref.WeakSet()替代普通集合来跟踪 Task:_live_tasks.add(asyncio.create_task(coro)),再定期检查len(_live_tasks)是否只增不减
排查时最容易被忽略的三个点
tracemalloc 快照要早打;gc.get_referrers(task) 要盯 frame 和 extra;所有 async with 必须包裹 try/except ——否则异常一出,__aexit__ 不执行,底层连接池、缓冲区、临时文件句柄全滞留。这些点不手动验证,光看 all_tasks() 数量会漏掉一半泄漏源。


















