<p>TaskGroup 的核心是结构化并发控制而非简化并发——它通过 async with 强制生命周期管理,要求所有子任务必须用 tg.create_task() 创建并自动收尾,异常用 except* 捕获,不保证结果顺序,本质是“关门清点”资源。</p>

TaskGroup 不是让并发“变简单”,而是让并发不崩得那么难看——它把原本靠人肉盯防的生命周期管理,变成语言级强制约束。
async with asyncio.TaskGroup() 是唯一合法入口
直接调用 asyncio.TaskGroup() 或在 async with 外保存 tg 引用,都会触发 RuntimeError: no active task group。这不是警告,是硬性拦截。
- 必须写成
async with asyncio.TaskGroup() as tg:,不能省略as tg—— 后续所有子任务都得通过它创建 - 不能把它塞进类属性、全局变量或传给另一个协程,一旦离开
async with块,tg就失效 - 哪怕你只打算启动一个任务,也得走这个流程:结构化不是可选项,是语法契约
tg.create_task() 和 asyncio.create_task() 的行为差异
前者把任务真正“交权”给上下文;后者仍是裸奔状态,容易漏收、漏 await、漏 cancel。
-
tg.create_task()创建的任务,会在组退出时自动await完成,或被统一取消并等待清理逻辑执行完毕 -
asyncio.create_task()创建的任务,若没显式await或没存引用,可能变成僵尸任务,持续占用连接或内存 - 你在
tg块内用asyncio.create_task(),它不会进组管理——这点极易忽略,且无任何提示
异常传播不是“汇总”,而是“熔断+收尾”
TaskGroup 抛出的是 ExceptionGroup,不是列表,也不能用普通 except ValueError 捕获单个子任务异常。
立即学习“Python免费学习笔记(深入)”;
- 任一子任务抛出非
CancelledError异常,其余任务立刻收到取消信号,但会等它们跑完async with或finally再聚合抛出 - 必须用
except*语法(如except* httpx.HTTPStatusError)匹配子异常,普通except会错过 - 如果子任务内部已处理异常并返回结果(比如返回
{"error": "timeout"}),那 TaskGroup 就不会中断整个组——是否熔断,取决于你让任务怎么“表达失败”
TaskGroup 不保证结果顺序,也不负责结果组装
它只管“生灭同步”,不管“谁先谁后”。你要顺序,得自己加标识。
-
tg.create_task(fetch_user(1))和tg.create_task(fetch_user(2))返回的Task对象,调用.result()时顺序不确定 - 别依赖列表索引对应原始调用顺序;推荐在协程里传入
id或key参数,让每个结果自带身份 - 没有内置的“按完成先后收集”机制;需要的话,得自己用
asyncio.as_completed()包一层,但那就脱离了 TaskGroup 的结构保障
最容易被忽略的点:TaskGroup 的价值不在“多开几个协程”,而在于“关门前清点人数”。只要有一个任务忘了释放锁、没关 HTTP 连接、没退出 async with database.transaction(),它就会拖垮整个组——而这种问题,只有在压测或上线后资源耗尽时才暴露。


















