Python TaskGroup 如何汇总多个子任务异常
Python 的 asyncio.TaskGroup 会在异步上下文退出时,把多个子任务产生的非取消异常组合成 ExceptionGroup(必要时是 BaseExceptionGroup)再抛出。处理时不要只写一个宽泛的 except Exception;更实用的方式是用 except* 按异常类型拆分子组,让校验错误、超时和其他故障分别进入对应分支。
- 第一个非
CancelledError的失败会触发其余任务取消,TaskGroup 会等待它们完成收尾。 - 最终汇总的是实际产生的非取消异常,不保证每个已启动任务都一定失败。
except*只消费匹配子组;没有匹配的异常仍会继续向外传播。
TaskGroup 改变的是失败边界
我在把一组裸 create_task() 改成 TaskGroup 时,最不适应的不是语法,而是失败不再属于某一个孤立任务。TaskGroup 把同一组子任务放进统一生命周期:其中一个任务首次以非取消异常失败时,组会取消尚未结束的兄弟任务,等待它们退出,然后在 async with 边界统一报告异常。
这项能力从 Python 3.11 加入,核心价值是结构化并发:任务的创建、等待、取消和失败都被限制在清晰的上下文里。对旧代码的直接影响是,原来只接收一个异常的处理逻辑,迁移后可能面对一个有层级的异常组。

最小写法:让 TaskGroup 在出口汇总异常
下面示例一次创建三个任务。子协程中的异常没有在内部吞掉,因此 TaskGroup 退出时会把已经发生的异常放进异常组。示例故意让协程立即失败,便于集中演示异常组;在真实 I/O 中,第一个失败通常会让还未失败的任务收到取消。
import asyncio
async def load_part(name: str) -> str:
"""模拟不同分片的读取失败。"""
if name.startswith("bad-"):
# 数据错误保留为 ValueError,便于 except* 分类匹配。
raise ValueError(f"{name} 数据格式错误")
# 超时使用独立异常类型,避免与数据错误混在一起。
raise TimeoutError(f"{name} 读取超时")
async def main() -> None:
try:
async with asyncio.TaskGroup() as group:
# 所有任务归属同一个结构化并发边界。
group.create_task(load_part("bad-orders"), name="orders")
group.create_task(load_part("bad-users"), name="users")
group.create_task(load_part("inventory"), name="inventory")
except* ValueError as group_error:
# group_error 仍是异常组,只包含匹配的 ValueError 子组。
for error in group_error.exceptions:
print(f"数据错误:{error}")
except* TimeoutError as group_error:
# 不同类型可以在同一个 try 后分别处理。
for error in group_error.exceptions:
print(f"超时错误:{error}")
asyncio.run(main())
except* 后得到的对象不是单个 ValueError,而是保留原有层级的匹配子组。这里直接遍历 exceptions 适合扁平示例;如果子任务内部又创建 TaskGroup,异常组可能嵌套,日志系统应保留组结构和 traceback。
用 except* 按类型拆分异常组
except* 的价值在于“按成员类型匹配”,而不是把整个异常组粗暴归为一种错误。一个异常组可以同时被多个 except* 分支处理:匹配的子组交给当前分支,剩余部分继续匹配后面的分支;最终仍未处理的异常会重新组合并向外传播。

这比在一个 except ExceptionGroup 里手动判断每个成员更稳妥,也能保留嵌套组的上下文。需要注意,同一个 try 不能混用普通 except 和 except*;而且 except* 必须写明匹配类型。
为什么有时只看到一个异常
“汇总多个异常”不等于“等待所有任务都自然失败”。TaskGroup 的安全策略是尽快结束同组工作:第一个普通异常出现后,未完成的兄弟任务会被取消。因此,最终异常组里有几个成员取决于真实并发时序:
| 场景 | 异常组中的典型结果 | 说明 |
|---|---|---|
| 多个任务几乎同时失败 | 可能包含多个异常 | 失败已发生,来不及被取消替代 |
| 一个任务先失败,其他任务仍在等待 I/O | 通常只有首个普通异常 | 其他任务收到取消并正常结束 |
| 兄弟任务在取消清理中又抛出普通异常 | 会把清理异常一并组合 | 清理代码应尽量可靠 |
| 任务仅以 CancelledError 结束 | 取消异常不作为普通成员汇总 | TaskGroup 用取消完成内部协作 |
这也是我排查“为什么异常组只有一项”时最先看的地方:先确认其他任务究竟已经失败,还是在首个错误后被取消。不能为了凑齐错误列表而吞掉取消;那会破坏 TaskGroup 的结构化并发语义。
取消处理不要吞掉 CancelledError
协程在取消时应通过 finally 释放资源。如果确实捕获 asyncio.CancelledError 做记录,清理后通常要继续 raise。官方文档明确提醒,TaskGroup 和 asyncio.timeout() 都在内部使用取消,吞掉取消可能让这些组件出现异常行为。
import asyncio
async def write_batch() -> None:
resource = await open_async_resource()
try:
# 真实写入逻辑可以在这里包含多个 await。
await resource.write()
except asyncio.CancelledError:
# 可以记录取消,但随后继续抛出,保留 TaskGroup 语义。
await resource.mark_cancelled()
raise
finally:
# 无论成功、失败还是取消都释放资源。
await resource.close()
KeyboardInterrupt 和 SystemExit 还有特殊规则:TaskGroup 仍会取消并等待其他任务,但随后重新抛出原始基础异常,而不是把它们当作普通 ExceptionGroup 交给业务分支处理。
TaskGroup 与 gather 应该怎么选
asyncio.gather(..., return_exceptions=True) 也能把异常作为结果返回,但它表达的是“收集每个位置的结果或异常”。TaskGroup 表达的是“这些任务属于同一个生命周期,任一关键失败应让整组收尾”。两者的差别不只是返回值形状。
| 需求 | 更适合的 API | 原因 |
|---|---|---|
| 全部操作相互独立,希望收集每一项结果 | gather(return_exceptions=True) | 结果与输入位置一一对应 |
| 任一失败后其余工作不应继续 | TaskGroup | 自动取消并等待兄弟任务 |
| 希望按异常类型统一处理 | TaskGroup + except* | 异常组能保留类型与嵌套结构 |
| 旧代码只依赖 gather 的顺序结果 | 谨慎迁移 | TaskGroup 的取消行为会改变任务生命周期 |
对我来说,TaskGroup 更适合“一批工作必须整体收口”的场景,例如并行加载同一请求所需的多个数据源;gather(return_exceptions=True) 更适合批量探测、独立检查这类允许部分失败的任务。
给每个异常补上任务上下文
异常组解决了“不要丢异常”,但不会自动把业务对象名称写进错误消息。生产代码最好在子协程边界补充上下文,并用 raise ... from exc 保留原始因果链。这样即使同一类型出现多次,也能知道是哪个数据源、文件或请求失败。
class PartLoadError(RuntimeError):
"""携带分片名称的业务异常。"""
async def load_with_context(part: str) -> bytes:
try:
# 调用真正的数据读取函数。
return await fetch_part(part)
except OSError as exc:
# 增加业务标识,同时保留底层异常链。
raise PartLoadError(f"分片 {part} 读取失败") from exc
迁移旧代码时,我建议先做一个最小验证:准备两个会立即失败的协程和一个会等待的协程,确认异常类型分流、兄弟任务取消、资源清理和未匹配异常传播都符合预期,再把 TaskGroup 放进真实请求链路。
相关问题
TaskGroup 会保证收集到每个子任务的错误吗?
不会。首个非取消异常会触发兄弟任务取消;只有已经发生或在收尾中产生的非取消异常才会进入异常组。
能否直接用 except Exception 捕获 ExceptionGroup?
ExceptionGroup 是 Exception 的子类,普通捕获可以拿到整组,但无法像 except* 那样按成员类型自动拆分。
except* 后拿到的是单个异常吗?
不是。处理器拿到的仍是保持层级的匹配异常子组,可以读取 exceptions 或保留整个组交给日志系统。
官方资料在哪里?
encoding/json/v2 的 omitzero 为什么没有省略字段
- 上一篇
- encoding/json/v2 的 omitzero 为什么没有省略字段
- 下一篇
- 用 encoding/json/v2 流式读取连续 JSON 值
-
- 文章 · python教程 | 5小时前 | 并发编程 · 工程实践 · Python教程 · 多进程日志 QueueListener multiprocessing.Queue RotatingFileHandler Python QueueHandler
- Python 日志 QueueHandler 解决多进程写入争用
- 186浏览 收藏
-
- 文章 · python教程 | 7小时前 | 数据校验 · python · Pydantic 部分更新 exclude_unset model_fields_set 显式空值 model_dump
- Pydantic 模型更新时区分未提供字段与显式空值
- 399浏览 收藏
-
- 文章 · python教程 | 9小时前 |
- pytest Fixture 作用域如何影响测试隔离与速度
- 341浏览 收藏
-
- 文章 · python教程 | 12小时前 | Python教程 · pathlib · 路径安全 Python pathlib Path.resolve 目录穿越 relative_to
- Pathlib 安全拼接用户路径:解析后再验证根目录
- 463浏览 收藏
-
- 文章 · python教程 | 14小时前 | 性能优化 · Python教程 · Python 进程间通信 pickle multiprocessing SharedMemory
- multiprocessing 传输大对象为何变慢,如何减少序列化
- 478浏览 收藏
-
- 文章 · python教程 | 1天前 | python · Python import很慢 -X importtime 模块级副作用 延迟导入 Python启动优化
- Python import 很慢怎么分析:模块级副作用与延迟导入
- 292浏览 收藏
-
- 文章 · python教程 | 1天前 | python · 异步编程 · Python asyncio contextvars request_id
- contextvars 在异步请求链中传递追踪信息
- 393浏览 收藏
-
- 文章 · python教程 | 1天前 | 并发 · 异常处理 · python · asyncio · CancelledError 结构化并发 ExceptionGroup Python asyncio TaskGroup asyncio gather
- asyncio TaskGroup 让并发任务在首错时一起收敛
- 246浏览 收藏
-
- 文章 · python教程 | 1天前 |
- Python 3.14 自由线程程序怎样显式保护共享状态
- 337浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 383次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 454次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 467次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 408次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 237次使用
-
- 物流异常件转派时如何保留原单号与处理时限
- 2026-09-20 276浏览
-
- Golang异常处理之defer,panic,recover的使用详解
- 2023-01-07 339浏览
-
- SingleFlight模式的Go并发编程学习
- 2023-01-01 285浏览
-
- Go并发编程之sync.Once使用实例详解
- 2022-12-27 484浏览
-
- 小学生也能看懂的Golang异常处理recover panic
- 2022-12-30 485浏览

