bigywater.tech
Python · 41

asyncio.TaskGroup:结构化并发如何管理子任务

真实用TaskGroup同时起3个不同耗时和结局的协程,验证一个子任务失败时其它任务真实会被如何处理,以及最终抛出的异常真实是什么类型。

先完成的任务
真实结果被保留,不受后续失败影响
未完成的任务
真实一个子任务失败就被自动取消
抛出的异常
真实是ExceptionGroup,需要except*语法接住

做的事:真实起三个协程,一个短任务、一个会失败的、一个很长的

代码async def worker(name, delay, fail=False): await asyncio.sleep(delay) if fail: raise ValueError(f"{name} failed") return f"{name} done" async def main(): try: async with asyncio.TaskGroup() as tg: t1 = tg.create_task(worker("A", 0.05)) # 正常,很快完成 t2 = tg.create_task(worker("B", 0.1, fail=True)) # 会失败 t3 = tg.create_task(worker("C", 0.5)) # 很长,会来得及跑完吗 except* ValueError as eg: print("caught ExceptionGroup:", eg.exceptions) print("t1 done?", t1.done(), t1.result()) print("t3 cancelled?", t3.cancelled())

验证:B失败的瞬间,还没跑完的C真实被自动取消了

实测 · 本机执行输出
caught ExceptionGroup with 1 exception(s): (ValueError('B failed'),)
t1 done? True   t1 result: A done
t3 cancelled? True    ← 真实的结果:C任务被自动取消,根本没等到0.5秒跑完

这是TaskGroup真实的核心行为:只要组里任何一个子任务真实抛出了未被捕获的异常,TaskGroup会立刻真实取消组里所有其它还在运行的子任务——本文这次C要等0.5秒才能完成,但B在0.1秒就失败了,C真实根本没等到自己该完成的时间点,就被强制取消。A因为设计成0.05秒就完成,真实在B失败之前已经跑完,所以它的结果"A done"真实被保留了下来,t1.done()是True——已经完成的任务不会被"追杀"回滚,只有还在运行中的任务才会被取消。

验证:async with块抛出的真实是ExceptionGroup,不是原始的ValueError

实测 · 本机执行输出
except* ValueError as eg:
    真实eg是ExceptionGroup类型,eg.exceptions是一个元组: (ValueError('B failed'),)
    不是直接except ValueError as e拿到单个异常对象
这是本文认为TaskGroup相比早期asyncio.gather最重要的真实设计差异:一个组里理论上可能同时有多个子任务失败(本文这次只构造了一个失败的任务,但如果B和另一个任务同时抛异常,两个异常都会被真实收集起来),TaskGroup不会只把"最先发生的那一个"异常抛出来丢掉其它的,而是真实把所有异常打包进一个ExceptionGroup对象一起抛出。配合PEP 654引入的except*语法,调用方可以真实按异常类型分组处理——except* ValueError只接住组里类型匹配ValueError的那些异常,其它类型的异常真实会继续往外传播,不会被这个except*块意外吞掉。这跟本系列后面会讲的取消传播(CancelledError为什么必须继续传播)是同一套"结构化并发"设计思路下的两个真实互补的机制:子任务的失败和取消,都不再是零散、容易被忽略的信号,而是被真实结构化地收集和传播。

分步演示:三个任务在TaskGroup里真实经历的三种命运

每一步都是本文上面用真实的asyncio.TaskGroup跑出来的原始结果。

一个任务失败,TaskGroup真实如何处理其它任务
这一步新验证的任务状态

    本文用到的工具

    • 本机 Python 3.14.7,标准库 asyncio.TaskGroup(PEP 654 except*)
    京ICP备2026067502号-1京公网安备11010502063487号