确认事件循环堆积未完成task可直接查看asyncio.all_tasks()集合大小,若远超预期且持续增长,说明存在未await或未cancel的task;需添加健康检查接口监控数量及最老task名称,并注意未捕获异常会导致task静默挂起。

怎么确认是事件循环里堆积了未完成的 Task
直接看 asyncio.all_tasks() 返回的集合大小,比预期多出几十甚至上百个,基本就是它。尤其在长时运行的服务(比如 FastAPI 后台任务、爬虫调度器)中,如果请求量上来了但并发没明显增加,len(asyncio.all_tasks()) 却持续增长,大概率有 Task 没被 await 或没被 cancel。
实操建议:
- 加个健康检查接口,返回
len(asyncio.all_tasks())和最老的几个Task的get_coro().__name__,方便快速定位源头 - 别只依赖
asyncio.create_task()就完事——如果 task 里抛了未捕获异常,它会静默“挂起”,既不结束也不报错,但一直占着引用 - 用
asyncio.current_task()在关键路径打日志,确认是否真正在执行,还是卡在某个await上(比如超时没设、下游服务假死)
循环引用怎么被协程放大成内存泄漏
Python 的垃圾回收器(GC)对循环引用很敏感,而协程对象天然容易形成闭包链:比如一个 async def 函数里引用了类实例,这个实例又通过属性反向持有该协程的 Task(常见于状态管理类或回调注册场景),GC 就无法自动清理。
实操建议:
- 用
gc.get_referrers(obj)查看谁在持有一个疑似泄漏对象的引用,重点盯Task、coroutine、frame这三类对象 - 避免在协程内部直接绑定实例方法作为回调(如
loop.call_later(1, self.on_done)),改用弱引用包装:weakref.WeakMethod(self.on_done) - 手动触发
gc.collect()并检查gc.garbage是否有残留——如果有,说明存在不可达但 GC 无法处理的循环引用
asyncio.run() 多次调用为什么会导致 Task 积累
每次调用 asyncio.run() 都会新建一个事件循环,但旧循环里的 Task 如果还没结束,它们不会自动迁移到新循环,也不会被强制清理——它们变成“孤儿 Task”,继续持有资源、引用和栈帧,直到进程退出。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
实操建议:
- 绝不在循环体或高频路径里反复调用
asyncio.run();生产环境应该只启动一次事件循环,用asyncio.get_event_loop()或asyncio.get_running_loop()复用 - 如果必须多次启动(比如单元测试),用
asyncio.run()前先确保前一个 loop 已关闭:asyncio.set_event_loop(None),并显式 cancel 所有 pending task - 注意
asyncio.run()内部会调用loop.close(),但前提是所有 task 都已完成;否则会抛RuntimeError: Event loop is closed,这个错误本身就会掩盖真正的泄漏点
用什么工具能快速定位泄漏源头
靠 print 日志太慢,靠 memory_profiler 又太重——真正好用的是组合:用 tracemalloc 定位分配位置 + objgraph 看引用关系 + asyncio.all_tasks() 看存活状态。
实操建议:
- 启动时加
tracemalloc.start(25),出问题后用tracemalloc.get_top_locations(10)找到协程创建最多的文件行号 - 用
objgraph.show_growth(limit=5)对比两次快照,看哪些类型(尤其是Task、coroutine、FrameType)在涨 - 对可疑
Task调用task.get_stack(),输出它的调用栈——很多泄漏就藏在某一行没加timeout的await asyncio.sleep()或await aiohttp.get()里
协程泄漏最难缠的地方不是找不到,而是它往往不报错、不崩溃、只悄悄吃内存;等你发现时,引用链可能已经嵌套五六层,这时候再回头补 weakref 或 cancel(),就得一层层拆闭包逻辑。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










