根本原因是Task被隐式持有(如all_tasks或全局集合),导致即使完成也不释放;协程帧、traceback、logging extra等会延长引用链,需用WeakSet监控、清理私有属性并避免大对象直引。

为什么 asyncio.create_task() 后 Task 对象不自动释放
根本原因不是你忘了 await,而是 Task 被隐式持有——最常见的是被 asyncio.all_tasks() 持有,或被未清理的全局集合(如字典、列表)长期引用。Python 的异步事件循环不会主动回收仍在运行或已取消但未被垃圾收集的 Task,尤其当它内部捕获了闭包变量、日志上下文或异常 traceback 时,引用链可能比你想象的更顽固。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
sys.getrefcount(task)查看当前引用数(注意:调用它本身会+1,需对比基线) - 在 Task 完成后立刻检查:
task.done() and not task.cancelled(),再确认task._coro.cr_frame是否为None(未清理的协程帧是内存泄露大户) - 避免在 Task 内部直接引用大对象(如 pandas DataFrame、文件句柄),改用 ID 或弱引用传递
如何用 weakref 监控 Task 生命周期并定位泄漏点
直接遍历 asyncio.all_tasks() 只能看到“活着”的 Task,但泄露往往发生在已结束却未被回收的 Task 上。这时要用 weakref.WeakSet 主动注册所有新建 Task,并在回调中记录销毁时间。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 在创建 Task 时统一包装:
task = asyncio.create_task(coro); _live_tasks.add(task),其中_live_tasks是weakref.WeakSet() - 定期检查
len(_live_tasks)是否持续增长,结合gc.get_referrers(task)找出谁在强持有它 - 特别注意 logging 模块:若 Task 中调用了
logger.info(..., extra={'task_id': id(task)}),而 logger 配置了持久化 handler,extra 字典可能把 Task 实例锁死
Task.cancel() 后内存仍不下降?检查 traceback 和 exception 存储
调用 task.cancel() 并不等于立即释放内存。如果 Task 在 await 点被取消,其 task.exception() 可能非空,而 Python 会把完整 traceback 对象绑定到 exception 上——这个 traceback 又持有着整个协程帧链,导致整条调用栈无法回收。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 在 cancel 后显式清理:
if task.cancelled(): task._exception = None; task._traceback = None(注意这是私有属性,仅用于调试和紧急修复) - 用
tracemalloc定位源头:tracemalloc.start(); ...; snapshot = tracemalloc.take_snapshot(),然后过滤"asyncio/tasks.py"相关分配 - 生产环境慎用
asyncio.shield()包裹长期运行的子协程——shield 会阻止外部 cancel 传播,导致 Task 卡在 pending 状态且无法被 all_tasks() 正确归类
用 asyncio.TaskGroup 替代手动 create_task 的实际效果
asyncio.TaskGroup(Python 3.11+)不是语法糖,它强制你在作用域退出时等待所有子 Task 结束或传播异常,天然规避了“忘记 await”和“Task 悬空”的问题。但它不能解决已有引用泄漏,只是收口了最常见的失控场景。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 迁移时不要只改语法:原代码中类似
tasks.append(asyncio.create_task(...))必须删除,改用async with asyncio.TaskGroup() as tg: tg.create_task(...) - TaskGroup 不捕获
KeyboardInterrupt和SystemExit,若你的服务依赖信号退出,需额外处理signal.signal(signal.SIGTERM, ...)并主动 cancel group - 老版本(create_task 返回的仍是标准
asyncio.Task,不改变底层引用行为
真正难排查的从来不是“哪个 Task 没被 await”,而是“谁悄悄把它的 traceback 塞进了某个全局 metrics 计数器里”。每次看到内存缓慢爬升,先 dump 一次 gc.get_objects() 筛选 asyncio.Task 实例,再对每个实例跑 gc.get_referrers() ——90% 的泄漏藏在第三层引用里。


















