当前位置:首页 > 文章列表 > 文章 > python教程 > asyncio TaskGroup 怎样收集并发异常

asyncio TaskGroup 怎样收集并发异常

来源:17golang原创 2026-10-25 13:35:43 0浏览 收藏

在 async with asyncio.TaskGroup() 外使用 except*,按异常类型处理异常组;需要逐条记录时,再递归遍历组内的异常叶子。TaskGroup 会等待组内任务收尾,并汇总实际发生的非取消异常,但它不会保证所有任务都运行到各自报错的位置。

本文用一个批次检查场景说明怎样统计异常:一项任务报告字段和附件错误,另一项任务等待并响应取消。示例为原创演示,未在本机执行;所有数量说明都是该示例的结构或语义预期,不是压测成绩。

官方文档:https://docs.python.org/3/library/asyncio-task.html#task-groups。异常组说明:https://docs.python.org/3/library/exceptions.html#exception-groups。语法参考:https://docs.python.org/3/reference/compound_stmts.html#except-star。

先定义观察指标:异常数不等于任务数

先把提交任务数、异常叶子数、取消任务数分开。两个任务并不意味着最多只有两个异常:某个任务可能抛出嵌套异常组。另一方面,任务被取消也不应计作一次附件读取失败。本文记录的是分类后的叶子数量,并单独观察 Task.cancelled()。

指标本例口径用途
提交任务数TaskGroup 创建的直接任务确定参与范围
异常叶子数递归去掉组容器后的异常统计实际错误
取消任务数退出后 cancelled() 为真解释未完成工作

建立假设:失败会取消同组未完成任务

普通子任务首次发生非 CancelledError 异常时,TaskGroup 会请求取消其他尚未完成的同组任务,等待收尾后抛出异常组。因此,“收集并发异常”的准确含义是收集退出前实际产生的错误,不能理解为强制每个任务完成全部检查。

示例的 batch_check 自身抛出一个包含 ValueError 和 OSError 的 ExceptionGroup;pending_job 则保持等待。ready 是两个协程共享的就绪约定,保证等待任务已经进入其工作范围。这个设计刻意避开用两个相近 sleep 时间来赌谁先抛错。

图1:TaskGroup、两个任务与共享 ready 对象的静态依赖
图1:TaskGroup、两个任务与共享 ready 对象的静态依赖。静态关系说明图,不是运行截图。

改动点:在 TaskGroup 外按类型处理异常组

保存以下代码为 taskgroup_errors.py,使用 Python 3.11 或以上版本。except* 捕获到的是匹配类型的异常子组,内部结构仍可能嵌套,所以 leaves 不直接把 errors.exceptions 当成平坦错误清单。

import asyncio


def leaves(error):
    # 异常组可以嵌套,递归后才是实际异常叶子。
    if isinstance(error, BaseExceptionGroup):
        for child in error.exceptions:
            yield from leaves(child)
    else:
        yield error


async def batch_check(ready):
    await ready.wait()
    # 一个子任务主动报告两个检查错误,不冒充两项独立并发失败。
    raise ExceptionGroup("批次检查失败", [
        ValueError("字段 region 无效"),
        OSError("附件读取失败"),
    ])


async def pending_job(ready):
    ready.set()
    try:
        # 保持挂起,便于观察同组失败引起的取消。
        await asyncio.Event().wait()
    finally:
        print("挂起任务清理完成")


async def main():
    ready = asyncio.Event()
    tasks = {}
    counts = {"value": 0, "io": 0}
    try:
        async with asyncio.TaskGroup() as tg:
            tasks["check"] = tg.create_task(batch_check(ready))
            tasks["pending"] = tg.create_task(pending_job(ready))
    except* ValueError as errors:
        # 只消费本例约定的输入错误,保留每个错误的类型和消息。
        for error in leaves(errors):
            counts["value"] += 1
            print(type(error).__name__, str(error))
    except* OSError as errors:
        for error in leaves(errors):
            counts["io"] += 1
            print(type(error).__name__, str(error))
    # 未匹配异常会继续传播,不会伪装成成功。
    print("已处理叶子数:", counts)
    print("取消任务数:", sum(task.cancelled() for task in tasks.values()))


asyncio.run(main())

这里的两个 except* 子句可以在同一次异常处理中分别执行。ValueError 子组交给字段类统计,OSError 子组交给读取类统计;未被匹配的错误继续向外传播。示例打印消息方便理解,实际服务应把记录送入项目日志系统,并明确哪些错误允许被消费。

图2:异常组、异常叶子和按类型处理器的包含与匹配关系
图2:异常组、异常叶子和按类型处理器的包含与匹配关系。静态关系说明图,不是运行截图。

复现方法:记录叶子异常和取消状态

# 在支持 TaskGroup 和 except* 的 Python 环境执行示例。
python3 taskgroup_errors.py

读者复现时先关注“挂起任务清理完成”,再关注各异常类型和计数。这个输出来自 finally 中的清理说明,并没有把取消转换成成功返回。示例没有持有真实网络连接;若换成实际客户端,资源关闭逻辑应放在相应的异步上下文或 finally 中。

生产排查时还应保留完整 traceback。只保存 str(error) 可能丢失调用位置、异常链和任务来源。可在业务异常中携带任务标识,或在创建任务时维护业务元数据;不要凭异常在组里的位置推断它属于第几个任务。

结果对比:看异常结构,不看虚构耗时

项目本例语义预期不能推出的结论
直接任务2 个不能代表并发容量上限
ValueError 叶子1 个不是所有输入错误都已遍历
OSError 叶子1 个不是独立 I/O 任务也必然报错
取消任务正常演示条件下为 1 个取消不等于普通业务失败

这里两个错误来自同一个批次检查任务,不能据此宣称 TaskGroup 总能同时捕获两个独立故障。如果换成两个各自等待 I/O 的任务,先出现的失败可能使另一个任务取消;实际异常叶子数量由已经发生的错误决定。

这项改动的价值在于异常不再被单条错误消息遮住,并且取消有独立口径。本文没有测量吞吐、内存和耗时,也不把异常处理结构的改动包装为性能提升。若业务真要衡量开销,应在自己的负载里区分成功路径、失败路径和清理成本。

边界条件与常见问题

为什么普通 except ValueError 没有处理到子错误?

外层对象是异常组,按子异常类型处理应使用 except*。同一个 try 语句不能同时混写普通 except 和 except*;如果必须处理不同层级,应使用嵌套 try 并明确各层职责。

能否把取消也放进失败清单?

可以另记取消状态,但不要把它误标为业务异常。CancelledError 属于 BaseException 层级,协程响应取消后通常应继续传播;误吞它可能破坏 TaskGroup 的收尾行为。KeyboardInterrupt 和 SystemExit 还有特殊传播规则,不宜用普通错误统计框架拦截。

想让每条记录都检查完怎么办?

那是不同的业务约定。可在单项任务内部把明确可恢复的校验错误转换为结构化结果,使任务正常返回;同时让未知程序错误继续抛出。不要仅在 TaskGroup 外增加捕获,就期待被取消的检查自动完成。

except* 处理后怎样保留失败状态?

本例为了展示计数,消费了已知类型。在服务入口应按业务决定返回失败对象、记录状态或重新抛出异常;“错误已记录”并不等于整个请求成功。统计、补偿和对调用方反馈应使用同一套业务口径。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
AES/GCM 怎样处理 AEAD 标签AES/GCM 怎样处理 AEAD 标签
上一篇
AES/GCM 怎样处理 AEAD 标签
asyncio.timeout 怎样转换超时异常
下一篇
asyncio.timeout 怎样转换超时异常
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    685次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    700次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    753次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    717次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    552次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码