登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python asyncio.TaskGroup 怎样在任务组结束时集中处理异常

来源:17golang原创

时间:2026-10-26 04:01:45 256浏览 收藏

将多个协程用TaskGroup.create_task加入任务组,在async with外层用except*捕获指定类型的异常子组。某个任务出现非取消异常后,TaskGroup会取消其余任务并等待它们结束,然后把已发生的非取消异常集中抛出;它不会为了收集所有业务错误而放任其余任务继续完成。

官方地址:https://www.python.org/

准备 Python 3.11 及以上环境

设想一个同时读取订单摘要和库存摘要的异步入口。如果由我维护这个入口,我会先明确:任何一项失败就让本次整体请求失败,还是允许各项独立失败。这里采用前一种约定,适合用TaskGroup。下面是为说明机制设计的本地模拟,未连接真实业务服务,也未在本机执行。

TaskGroup和except*从Python 3.11开始提供。保存示例为taskgroup_demo.py,使用Python 3.11及以上解释器,无需安装第三方库。不要把except*代码交给Python 3.10解析。

TaskGroup作用域与子任务归属结构说明图
结构说明图:任务归属于同一个TaskGroup,异常出口位于组作用域之外;不是运行截图。

把任务与异常出口放在同一个作用域

把try放在async with外面才能覆盖退出任务组时抛出的异常。示例让订单任务抛出ValueError,库存任务进入可取消的等待;finally代表资源清理的位置。

import asyncio

async def load_summary(name, delay, invalid=False):
    try:
        await asyncio.sleep(delay)  # 模拟可取消的异步等待
        if invalid:
            raise ValueError(f"{name}: 摘要格式错误")
        return {"name": name, "items": 3}
    finally:
        # 实际项目在此关闭本任务持有的资源。
        print(f"{name}: 清理结束")

def leaves(group):
    # 子组可以继续嵌套,递归取得叶子异常。
    for error in group.exceptions:
        if isinstance(error, BaseExceptionGroup):
            yield from leaves(error)
        else:
            yield error

async def main():
    tasks = {}
    try:
        async with asyncio.TaskGroup() as group:
            tasks["订单"] = group.create_task(
                load_summary("订单", 0.02, invalid=True), name="orders"
            )
            tasks["库存"] = group.create_task(
                load_summary("库存", 1.0), name="inventory"
            )
    except* ValueError as errors:
        for error in leaves(errors):
            print(f"输入问题: {error}")
    else:
        # 仅在任务组正常完成时读取全部结果。
        print({key: task.result() for key, task in tasks.items()})

if __name__ == "__main__":
    asyncio.run(main())

except* ValueError匹配异常组中的ValueError叶子,并保留子组结构,所以errors.exceptions不保证是扁平列表。辅助函数leaves只做遍历,便于逐条记录。本例没有用except* Exception吞掉所有未知问题。

任务组正常完成后才能直接按成功路径读取结果;若已经处理过异常,不能随后无条件对全部任务调用result(),因为其中可能有失败或被取消的任务。

运行时观察取消与异常分组

读者可运行以下命令:

python3 taskgroup_demo.py

预期会看到任务清理消息和“输入问题”消息,而不会打印整批成功结果。输出行的具体顺序与调度有关,不应据此推导任务执行顺序;这里提供的是阅读判断标准,没有提供伪造的实测输出。

发生的情况组退出时的含义
所有任务成功正常离开async with,可读取结果
一个任务抛ValueError其余未完成任务被取消,等待退出后抛异常组
多个任务在取消生效前已经失败已发生的非取消异常可共同进入异常组
任务因组内取消而结束CancelledError通常不作为普通失败加入异常组

集中处理并不保证收集到每个任务原本可能产生的错误。库存任务可能在到达自己的业务检查之前就已取消。若需求是每项都独立完成并形成逐项结果,应在任务内部把预期业务失败转换为明确结果对象,而不是误用TaskGroup的失败即取消语义。

异常组与类型子组静态结构说明图
结构说明图:异常组可包含不同类型及嵌套子组,except*按叶子异常类型筛选;不表示每次都会出现多种失败。

扩展实验:保留未知异常和部分结果

可以把订单任务的ValueError改成RuntimeError,理解未知异常会继续向上传播。若组内同时存在ValueError与其他未匹配异常,except*只处理匹配部分,剩余部分仍会抛出。不要用裸except把外层请求伪装成成功。

另一个实验是给多个任务设置相近延迟并让它们分别失败。实际能收集多少个异常取决于失败与取消的时机,不能依赖固定数量或固定排序,也不应根据exceptions列表的位置反推任务身份。业务异常消息、附加字段或任务侧日志应携带自己的业务标识。

如果确实要保留部分成功结果,在处理完异常之后逐个判断task.cancelled(),再检查task.exception(),最后才读取task.result()。此时TaskGroup已经等待组内任务结束,判断应发生在组退出之后。部分成功如何对外返回仍是业务接口约定,不能靠捕获异常自动决定。

TaskGroup只管理通过该组create_task创建的任务。若子协程另用asyncio.create_task生成游离任务,不能假设它也会被当前组完整管理。

退出任务组后的清理与常见问题

资源释放放进finally或异步上下文管理器。若显式捕获asyncio.CancelledError做清理,通常应在清理后重新raise;吞掉取消可能干扰TaskGroup的结构化并发行为。CancelledError继承BaseException,普通except Exception不会捕获它。

为什么只有一个失败也会进入except*?

TaskGroup退出时可以把单个普通失败包装成异常组;except*面对的不是“必须至少两个错误”的特殊情况。

能否在同一个try中同时写except和except*?

不能。同一try的处理分支不能混用两种语法,需要嵌套try安排不同边界。except*分支内部也不能使用return、break或continue。

KeyboardInterrupt也会变成普通异常组吗?

KeyboardInterrupt和SystemExit有特殊规则:任务组会取消并等待其他任务,然后重新抛出原始异常。外部取消也不是应当随手忽略的业务失败。

async with内部主体抛错怎么办?

组主体的非取消异常也会触发取消与等待,并可能与任务异常一起被分组。把捕获边界放在整个async with外侧可以覆盖这一出口。

本文只用本地协程与标准库,实验结束后无需清理外部服务。真正接入网络客户端时,每个客户端、连接或事务仍应有明确的释放与失败语义。

官方资料:TaskGroup任务与异常契约、异常组与except*说明。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>