“Task was destroyed but it is pending!”警告是因丢弃Task引用致GC回收时自动cancel,并非Python主动终止;CPython 3.11+默认启用该行为,典型原因是create_task()后未保存引用,如单独调用、赋None或函数内局部创建后返回。

Task was destroyed but it is pending! 是什么在销毁任务
这个警告不是 Python 主动“杀掉”了任务,而是你丢弃了 asyncio.Task 对象的引用,导致它被 GC 回收时自动调用 __del__ → 触发 cancel() → 打印警告。CPython 3.11+ 默认启用该行为,所以升级后突然满屏报这个,大概率不是代码变错了,是你一直没 hold 住 Task 引用。
- 典型写法:
asyncio.create_task(some_coro())单独一行,没赋值给任何变量 - 类中误写:
self._task = asyncio.create_task(...),但后续又执行了self._task = None,旧 Task 失去唯一引用 - 函数内创建后直接 return,局部变量生命周期结束,Task 被回收
为什么 await 一个 task 就不报了
await task 本质是让当前协程等待其完成,这期间 task 对象至少被当前栈帧强引用着;而更关键的是:await 行为本身会参与事件循环调度链路,使 task 的生命周期与调用方显式绑定。不 await,也不保存引用,就等于告诉 Python:“这个对象我不要了”,哪怕它还在跑、还在等网络响应。
- 注意区分:
await some_coro()和await asyncio.create_task(some_coro())效果不同:前者是同步等待,后者是并发启动 + 显式等待 - 如果只想 fire-and-forget,必须用其他方式维持引用(见下一条),不能靠“不 await 就没事”来侥幸
- 即使 task 已完成,只要没被 await 或检查
done(),它的结果和异常仍可能滞留——这也是潜在泄漏点
怎么安全地让 task “自生自灭”而不报警
真要后台运行又不想管,就得主动接管生命周期,而不是放任 GC 处理。核心是:让 task 至少被一个长期存活的对象持有,并在退出前清理。
- 存入实例属性:
self._background_task = asyncio.create_task(self.worker()),并在__aexit__或close()中if self._background_task and not self._background_task.done(): await self._background_task - 用集合管理(谨慎):
ACTIVE_TASKS.add(asyncio.create_task(...)),并在 task 完成回调里.discard();漏删会导致内存持续增长 - 用
asyncio.create_task(..., name="xxx")配合 debug 模式定位来源,但 name 不解决引用问题
debug=True 真正帮你定位什么
开启 asyncio.run(main(), debug=True) 后,警告会附带两行关键信息:task 创建位置(哪行 create_task)、当前挂起在哪一行 await。这才是救命线索——否则你只能看到“pending”,却不知道它卡在 aiohttp.ClientSession.get() 还是 asyncio.sleep()。
立即学习“Python免费学习笔记(深入)”;
- 环境变量也有效:
PYTHONASYNCIODEBUG=1 python script.py - 子线程中误用
get_running_loop()也会触发更详细的跨线程警告,比如 “coroutine created in thread X but awaited in thread Y” - debug 模式不会让程序变慢,但会增加日志量;上线前应关闭


















