objgraph可识别未回收的asyncio.task和协程对象,需结合快照对比、引用链追踪定位泄漏源,重点监控pending状态task及其frame局部变量。

objgraph 能看到哪些协程对象没被回收
协程对象(coroutine)和任务(asyncio.Task)一旦启动但没被 await 或 cancel,就可能长期驻留在内存里。objgraph 不会自动过滤“活跃协程”,它只管引用关系——所以你得主动筛出那些状态为 PENDING 却没被任何变量引用、又没被 event loop 清理的 Task 实例。
常见驻留对象包括:
-
asyncio.Task(尤其用asyncio.create_task()启动后忘记 await 或未加异常处理) - 被闭包捕获的协程函数内部变量(比如在
async def handler(): ...里定义的大字典,又被返回的Task引用) - 未清理的
asyncio.Queue或asyncio.Event,它们常被协程长期持有引用
用 objgraph 抓住泄漏源头的三步操作
别一上来就 objgraph.show_growth()——它对协程不敏感。要定位泄漏,得结合时间点快照 + 引用链追踪。
推荐流程:
- 在怀疑泄漏前,调用
objgraph.take_snapshot()记录 baseline - 执行若干轮异步逻辑(比如模拟 100 次 HTTP 请求),再调用一次
objgraph.take_snapshot() - 用
objgraph.get_diff()查看新增的Task、coroutine、frame数量;若Task持续增长且len(asyncio.all_tasks())不降,基本可确认泄漏
然后针对新增的 Task 实例,用 objgraph.find_backref_chain(task, objgraph.is_proper_module) 往上找谁持有了它——大概率是某个未 await 的 create_task() 调用点,或某个忘了 await queue.join() 的消费者协程。
为什么 asyncio.current_task() 和 task.cancel() 有时不管用
不是所有 Task 都能被 asyncio.current_task() 拿到,尤其当它运行在非当前 event loop 线程、或已被移出任务集但引用未断时。更关键的是:task.cancel() 只是设 flag,真正清理依赖协程体内的 except CancelledError 和资源释放逻辑。
典型失效场景:
- 协程里用了阻塞调用(如
time.sleep()),导致CancelledError无法及时抛出 - 协程处于
await asyncio.Lock.acquire()等待状态,cancel 后仍卡在等待队列中 - 任务被
asyncio.create_task()启动后,直接丢给后台,没存变量也没 await,后续连task.cancel()都无从下手
这种情况下,objgraph 是唯一能告诉你“这个 Task 还活着,而且它的 cr_frame.f_locals 里还攥着一个 50MB 的 response_data” 的工具。
避免误判:协程对象本身不等于内存泄漏
刚创建的 coroutine 对象是惰性的,不执行就不会分配栈帧,也不算泄漏。objgraph 里看到大量 coroutine 类型,得先用 coro.cr_running 或 coro.cr_await 判断是否真在运行或挂起;否则可能把临时生成又即将被 GC 的协程当成问题。
真正该盯的是:
-
Task实例数量持续上升(对比len(asyncio.all_tasks())) - 某个
Task的_state是PENDING,但其cr_frame显示卡在await asyncio.sleep(3600)这类长延迟点 - 通过
objgraph.show_backrefs([task], max_depth=3)发现它被一个全局dict或 class instance 意外强引用
协程生命周期短、调度频繁,靠人工 log 很难串起上下文。objgraph 不是万能的,但它给出的引用路径,往往就是那个忘了加 finally: lock.release() 的 2 行代码的位置。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











