当前位置:首页 > 文章列表 > 文章 > python教程 > Python asyncio TaskGroup 如何收口异常:取消传播与部分结果处理

Python asyncio TaskGroup 如何收口异常:取消传播与部分结果处理

来源:17golang原创 2026-08-27 07:22:42 0浏览 收藏

批量请求三个下游服务时,最麻烦的情况不是某一个请求失败,而是失败之后另外两个任务还在继续写入共享状态。Python 3.11 引入的 asyncio.TaskGroup 会把这件事收紧:一个子任务抛出未处理异常,其他兄弟任务会被取消,离开代码块时再以聚合异常的形式交给调用方。

要点速览

  • TaskGroup 适合“一组任务要么一起完成、要么整体收口”的并发批次。
  • 兄弟任务收到取消后,清理逻辑应放在 finally,不要吞掉 CancelledError。
  • 需要保留成功结果时,把结果写入批次上下文,并在外层用 except* Exception 统一整理。
  • 失败任务已经决定批次结果后,不要继续把半成品标记为可发布或可结算。

先看一个会失控的批量调用场景

假设一次批处理要同时读取库存、价格和优惠券。库存任务先成功,价格任务抛出异常,优惠券任务却还在等待网络返回。如果外层只捕获价格异常,最终很容易留下“库存已写入、价格未更新、优惠券稍后又回来覆盖状态”的半成品。

TaskGroup 的关键不是让请求更快,而是把这组任务的生命周期绑定在同一个上下文里。代码块退出前,组内任务必须结束;其中一个任务失败时,未完成的兄弟任务会收到取消信号。

TaskGroup 的最小可用写法

import asyncio

async def fetch_stock():
    await asyncio.sleep(0.05)
    return {"sku": "A-100", "stock": 8}

async def fetch_price():
    await asyncio.sleep(0.02)
    raise RuntimeError("price service unavailable")

async def fetch_coupon():
    try:
        await asyncio.sleep(0.30)
        return {"discount": 10}
    finally:
        print("coupon task cleanup")

async def load_product():
    async with asyncio.TaskGroup() as group:
        stock_task = group.create_task(fetch_stock())
        price_task = group.create_task(fetch_price())
        coupon_task = group.create_task(fetch_coupon())

    return stock_task.result(), price_task.result(), coupon_task.result()

async def main():
    try:
        result = await load_product()
        print(result)
    except* Exception as errors:
        print("batch failed:", errors)

asyncio.run(main())

运行后,fetch_price() 的异常会让 fetch_coupon() 被取消,控制台仍会执行它的清理分支。load_product() 不会在组内任务尚未收口时提前返回,这就是它和“手动 create_task 后随便 gather”最重要的边界。

Python TaskGroup 中价格任务失败后取消优惠券任务并进入清理阶段的数据生命周期示意图

取消传播:为什么 finally 比 except 更可靠

兄弟任务收到的是 CancelledError。如果任务持有连接、临时文件或批次锁,清理动作应放在 finally 中;如果确实需要记录取消原因,可以先捕获,再继续抛出:

async def worker(resource):
    try:
        await resource.run()
    except asyncio.CancelledError:
        await resource.mark_cancelled()
        raise
    finally:
        await resource.close()

这里的 raise 不能省略。吞掉取消异常会让 TaskGroup 误以为任务正常结束,调用方就可能把一个已经取消的批次当成成功。只有在非常明确的恢复场景下,才考虑 uncancel() 等更特殊的处理。

想保留部分结果,先区分“结果”与“成功状态”

TaskGroup 失败并不意味着所有已经完成的计算都不存在。可以让每个任务把自己的结果写入一个普通字典,但这个字典只能作为诊断或重试输入,不能直接代表批次成功。

async def run_batch():
    results = {}

    async def capture(name, operation):
        try:
            results[name] = await operation()
        except asyncio.CancelledError:
            results[name] = {"state": "cancelled"}
            raise
        except Exception as exc:
            results[name] = {"state": "failed", "error": str(exc)}
            raise

    try:
        async with asyncio.TaskGroup() as group:
            group.create_task(capture("stock", fetch_stock()))
            group.create_task(capture("price", fetch_price()))
            group.create_task(capture("coupon", fetch_coupon()))
    except* Exception as errors:
        return {"state": "failed", "results": results, "errors": str(errors)}

    return {"state": "ok", "results": results}

这个写法有两个收口点:每个任务负责留下可解释的局部状态,外层负责决定整批是 ok 还是 failed。不要因为字典里已经有库存结果,就把整个订单批次写成完成。

Python TaskGroup 将已完成、失败和取消结果分开记录并由外层统一收口的示意图

异常聚合与重试边界

TaskGroup 离开上下文时可能抛出 ExceptionGroup。使用 except* 可以按异常类型分拣,例如把网络错误放进一次批次重试,把数据校验错误直接送入人工处理。

try:
    await run_batch()
except* TimeoutError as timeout_errors:
    await schedule_retry("network", timeout_errors)
except* ValueError as data_errors:
    await record_bad_input(data_errors)

重试也要有边界:只重试可恢复的下游错误,并携带批次 ID 做幂等;不要在 except* 中无条件再次创建同一组任务,否则一次故障会被放大成重试风暴。

三个常见误区

把 TaskGroup 当成 gather 的换名字

gather() 的异常与取消语义要结合参数理解,而 TaskGroup 更强调结构化并发的生命周期。选哪个取决于调用方是否需要“一组任务绑定在一起”的失败边界。

在取消分支里只打印日志

日志不能替代状态清理。连接、事务和临时资源需要明确关闭;处理完后继续抛出取消异常,才能让外层正确收口。

把部分结果直接当成功结果返回

部分结果适合诊断、补偿和重试。对外返回前应有一个明确的批次状态字段,否则调用方很难区分“只完成了一半”和“全部成功”。

相关问题

TaskGroup 从哪个 Python 版本开始可用?

它是 Python 3.11 标准库 asyncio 的能力。更老的运行时需要升级,或继续使用现有并发方案并自行约定取消和收口规则。

一个任务失败时,已经完成的任务会被撤销吗?

TaskGroup 不会自动撤销外部副作用。它只负责任务生命周期和异常传播;数据库写入、消息发送等操作仍要用事务、幂等键或补偿逻辑保护。

什么时候更适合使用 gather?

如果任务之间相互独立,调用方希望按返回顺序收集结果,且已有清晰的异常策略,gather() 仍然直接。需要绑定取消、清理和整体完成边界时,再优先考虑 TaskGroup。

把收口规则写进批处理协议

TaskGroup 真正解决的是边界:谁创建任务,谁等待任务,谁决定批次状态,谁负责清理。把这四件事固定下来,再把部分结果和失败原因作为独立字段保存,并发代码就不容易在异常路径上留下“看起来完成、实际上未完成”的状态。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Java 反射调用私有方法为什么失败:MethodHandles 查找模式与模块边界Java 反射调用私有方法为什么失败:MethodHandles 查找模式与模块边界
上一篇
Java 反射调用私有方法为什么失败:MethodHandles 查找模式与模块边界
Go map.clear 怎么处理临时缓存:复用容量、引用释放与基准核对
下一篇
Go map.clear 怎么处理临时缓存:复用容量、引用释放与基准核对
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    432次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    508次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    524次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    464次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    294次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码