asyncio.taskgroup 是 python 3.11 引入的结构化并发工具,用于在任一子任务失败时自动取消其余任务,适用于需全成功才继续的服务端请求编排等场景。

asyncio.TaskGroup 是什么,什么时候该用它
asyncio.TaskGroup 是 Python 3.11 引入的正式替代 asyncio.gather 的结构化并发工具,核心价值不是“更炫”,而是“出错时能自动取消其余任务”。如果你写的是服务端请求编排、批量 API 调用、或任何需要“全成功才继续,任一失败就立刻停手”的场景,它比 asyncio.gather 更可靠。
常见错误现象:用 asyncio.gather 调多个 HTTP 请求,其中一个超时或 500,其余还在傻跑,浪费连接、拖慢响应、甚至触发下游限流。
使用场景:
- 启动一组相互独立但需统一生命周期的任务(比如同时拉取用户资料、订单、权限)
- 需要精确控制异常传播(不希望一个子任务的
ValueError被吞掉,也不希望另一个还在运行) - 任务之间不需要显式通信或共享状态(那是
asyncio.Queue或asyncio.Event的事)
注意:它不能替代 asyncio.create_task 手动管理——那是更底层的控制;TaskGroup 是帮你管“一批任务”的生命周期,不是单个任务的调度器。
立即学习“Python免费学习笔记(深入)”;
怎么写一个安全的 TaskGroup 调用
关键不是“怎么启动”,而是“怎么避免意外泄露或提前退出”。
- 必须在
async with asyncio.TaskGroup()语境中调用create_task,否则会报RuntimeError: no active task group - 不要在
with块外保留对子任务的引用,尤其别把它存进列表再 await —— 这样会绕过TaskGroup的取消机制 - 如果某个子任务抛出未捕获异常,
TaskGroup会立即取消所有其他正在运行的子任务,并把第一个异常原样抛出(不会打包成ExceptionGroup,除非有多个并发异常)
async def fetch_user(user_id):
async with aiohttp.ClientSession() as session:
async with session.get(f"/api/user/{user_id}") as resp:
return await resp.json()
<p>async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_user(1))
task2 = tg.create_task(fetch_user(2))
task3 = tg.create_task(fetch_user(3))</p><h1>✅ 正确:离开 with 块时,所有任务已完成或被取消</h1><pre class='brush:python;toolbar:false;'>return [task1.result(), task2.result(), task3.result()]TaskGroup 和 gather 的参数行为差异
最常被忽略的一点:gather 默认不传播 cancel,TaskGroup 默认传播且不可关。
-
asyncio.gather(*aws, return_exceptions=False):任一子协程被 cancel,其余照常运行;设return_exceptions=True可捕获异常但不中断执行 -
asyncio.TaskGroup没有这类开关——只要有一个出错或被 cancel,全部强制取消,且无法禁用该行为
性能影响很小,但兼容性要注意:
- Python TaskGroup,得降级用
gather+ 手动 cancel 逻辑(麻烦且易漏) - 如果你依赖
gather的“容忍部分失败”特性,强行换TaskGroup会导致行为突变,别为了新语法硬切
容易被忽略的取消边界和嵌套陷阱
TaskGroup 的取消是“向下穿透”的,但只穿透到它直接创建的子任务,不穿透子任务内部 spawn 的任务。
常见坑:
- 在
tg.create_task()里又调了asyncio.create_task()—— 这个孙子任务不会被TaskGroup管理,cancel 时会漏掉 - 子任务里用了
asyncio.to_thread或loop.run_in_executor,线程/进程里的工作不会被自动取消(这是 asyncio 本身限制,不是TaskGroup的锅) - 嵌套
TaskGroup是允许的,但外层 cancel 时,内层会收到 cancel 并尝试清理自己,如果内层没写好 finally/cancel handler,可能残留资源
真正难处理的,从来不是怎么开一个 TaskGroup,而是确认每个子任务是否真的“干净退出”——尤其是涉及文件句柄、数据库连接、长连接心跳这种。










