TaskGroup 是为解决 gather 根本缺陷而设计的结构化并发边界,强制生命周期管理、统一异常传播(ExceptionGroup)与协作式取消,必须用 async with,且子协程需主动响应取消。

TaskGroup 不是“更好用”的 gather,它是为解决 gather 根本性缺陷而生的结构化并发边界——它不追求写起来省几行,而是防止你漏掉取消、清理、异常传播这些一出错就难排查的事。
asyncio.TaskGroup 必须用 async with,否则直接报错
这是最常踩的第一个坑:asyncio.TaskGroup() 不能直接实例化,tg = asyncio.TaskGroup().__enter__() 也不行。唯一合法写法只有:
async with asyncio.TaskGroup() as tg:
tg.create_task(some_coro())
一旦离开这个块,tg 就失效;把 tg 存到类属性或全局变量里再调用 create_task,会立刻抛 RuntimeError: TaskGroup is closed。这不是限制,是强制你划清生命周期边界。
TaskGroup 抛的是 ExceptionGroup,不是单个异常
哪怕只有一个子任务失败,async with 块退出时抛出的也是 ExceptionGroup,老式 except ValueError: 完全捕获不到。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
立即学习“Python免费学习笔记(深入)”;
- 必须用 Python 3.11+ 新增的
except* ValueError:语法 - 如果多个任务分别抛
TimeoutError和JSONDecodeError,except* TimeoutError:只处理前者,后者继续向上冒泡 - 不能在
async with块内对单个tg.create_task()包try/except来“吞异常”——这会破坏组内统一熔断逻辑
tg.create_task() 和 asyncio.create_task() 的行为差异
表面都是启动协程,但背后控制权完全不同:
-
tg.create_task()创建的任务,其取消信号由 TaskGroup 统一调度:任一失败 → 全部收到CancelledError→ 等待它们响应(比如关连接、释放锁)→ 再退出 -
asyncio.create_task()创建的任务,一旦变量被覆盖或没显式 await,就变成“僵尸任务”,event loop 里挂着不释放资源 - 你可以在
async with块里任意位置动态调用tg.create_task(),比如在某个 API 返回后决定是否拉下一页,而gather要求所有协程提前列好
TaskGroup 的“慢”其实是它在尽责
压测时发现比 asyncio.gather() 慢几十毫秒?别急着换回去。那多出来的等待时间,是在等子任务真正做完清理:
- HTTP 请求任务里有
async with aiohttp.ClientSession()?TaskGroup 会等 session 关闭后再退出 - 数据库写入任务用了
async with db.transaction()?它会等事务 commit/rollback 完成 - 如果子任务里没写
try/finally或忽略task.cancelled(),这个“等待”就真成空等了——这不是 TaskGroup 的问题,是你协程没响应取消
真正的复杂点不在语法,而在你写的每个子协程是否真正支持协作式取消。不改协程本身,只换 TaskGroup,等于给敞篷车装安全带——约束有了,但人还得自己系上。

















