asyncio.TaskGroup 通过 async with 语法强制任务生命周期管理,防止任务泄露、异常静默和资源不释放:自动等待/取消子任务、聚合异常为 ExceptionGroup、禁止手动 cancel,仅限 Python 3.11+。

因为 asyncio.TaskGroup 强制你用 async with 定义任务边界,从语法层面堵死了“任务泄露”“异常静默”“资源不释放”这三类最常见又最难排查的异步漏洞。
TaskGroup 怎么防止任务泄露
手动调用 asyncio.create_task() 时,任务对象一旦脱离变量引用(比如在循环里反复赋值覆盖、没存进列表、或忘了 await),就变成 event loop 里挂着的“僵尸任务”。它还在跑,但你再也拿不到句柄去等或取消。
TaskGroup 把所有任务创建收口到 tg.create_task(),且只允许在 async with asyncio.TaskGroup() as tg: 块内调用。离开这个块时,它会自动等待所有子任务完成或响应取消——你根本没法漏掉。
- 不能在
with外保存tg引用,也不能把tg.create_task()返回的Task对象传到块外再await,否则破坏结构约束 - 直接实例化
asyncio.TaskGroup()会立刻报RuntimeError: no active task group - 想提前退出?
return或break就行,收尾逻辑由上下文自动触发
为什么异常不再静默,也不再丢
asyncio.gather() 遇到第一个异常就抛出,其余任务继续后台运行;而 TaskGroup 会向所有存活子任务发 cancel 信号,并**等待它们执行完 async with 和 finally 清理逻辑**,再聚合异常。
立即学习“Python免费学习笔记(深入)”;
这意味着:文件句柄、HTTP 连接、数据库事务、锁……只要协程里写了正确清理,就不会被跳过。
- 失败时抛的是
ExceptionGroup,不是单个异常(除非只有一个失败) - 必须用
except*捕获,except Exception会漏掉 - 若多个子任务同时抛
httpx.HTTPStatusError,except* httpx.HTTPStatusError能一次性拿到全部实例
tg.create_task() 和 asyncio.create_task() 的关键区别
前者不是后者的封装,而是生命周期托管接口:它把任务的“生杀大权”交给了 TaskGroup 实例,而不是让你自己扛。
你调用 tg.create_task() 启动的任务,其取消行为完全受组内统一调度——任一失败,其余全 cancel;你无法对其中某个单独 task.cancel(),那会破坏组一致性。
-
tg.create_task()支持在with块内任意位置动态调用(比如在另一个子任务await后、if分支里、甚至解析完响应才决定是否拉下一页) - 返回的
Task对象只能用于调用.result()或检查状态(如.done()),不能手动.cancel() - 别再写
results = [t1.result(), t2.result()]并指望顺序对应原始调用——顺序不保证,要靠任务名或显式标识区分
容易被忽略的兼容性与使用前提
它只存在于 Python 3.11+,且不是“升级就能用”的透明特性:你得主动改代码结构,否则旧逻辑照常泄露、照常丢异常。
- 旧版本需回退到
asyncio.create_task()+asyncio.wait()组合,但无法复现TaskGroup的清理等待语义 - 不能和
return_exceptions=True共存——TaskGroup不支持这个参数,这是设计取舍:它要的是确定性,不是容错灵活性 - 如果你的任务全是纯计算、无 I/O、无资源持有,那
TaskGroup的“额外等待”几乎不可测;但只要涉及连接、文件、锁,它的“慢”就是你在为安全付费


















