taskgroup 的核心价值是保障并发安全而非提升性能,它通过强制等待任务清理、传播取消信号、约束生命周期来防止资源泄露和异常静默,虽在部分场景下比 gather 稍慢,但换来的是可预测、可调试、可维护的并发行为。

TaskGroup 不是为提升“性能”而生的,它根本没优化吞吐、延迟或 CPU 占用——它的目标是**让并发行为可预测、资源不泄露、异常不静默**。如果你在压测中发现 TaskGroup 比 asyncio.gather 慢一点点,别慌,这不是 bug,是它在做你原来没做的事:等清理、传取消、守边界。
TaskGroup 的取消传播机制会拖慢“最快失败路径”
当你启动 10 个 HTTP 请求,第 2 个在 100ms 后抛出 ValueError,asyncio.gather(return_exceptions=False) 会在那一刻立刻抛异常、返回,其余任务继续后台跑(可能泄漏连接);而 TaskGroup 会立刻向剩下 8 个发送 cancel 信号,并**等待它们响应并退出**(比如关掉 aiohttp.ClientSession 或释放锁),这个等待过程就是额外开销。
- 这不是性能缺陷,是安全成本:你省下的那几十毫秒,可能换来未关闭的 TCP 连接、卡住的数据库事务、或没 flush 的日志
- 实际影响取决于子任务是否实现 cancellation 响应——协程里没写
try/finally或没检查task.cancelled(),这个“等待”就变成空等 - 若所有子任务都轻量且无状态(比如纯计算),差异几乎不可测;但涉及 I/O、资源持有时,
TaskGroup的“慢”恰恰是它在尽责
为什么不能用 asyncio.create_task + 手动 await 替代 TaskGroup
手动管理看似自由,实则极易漏掉关键环节:
- 忘记在
except块里await task—— 任务被取消后残留为“僵尸”,event loop 里还挂着引用 - 在循环中反复
create_task但只保留最后一个变量,前面的任务对象被丢弃,无法 await,也无法取消 - 多个子任务共享一个
async with httpx.AsyncClient()实例,但没统一生命周期约束,某个任务崩溃时 client 可能提前 close,其他任务直接RuntimeError: <client> is closed</client> -
TaskGroup强制你把所有create_task放在async with块内,天然规避了这些引用丢失和生命周期错位
TaskGroup 和 gather 的返回值行为差异直接影响错误处理逻辑
asyncio.gather 默认返回结果列表,顺序固定;TaskGroup 不返回任何东西,你得自己调用 task.result() 拿结果,而且顺序不保证。
- 这意味着你不能再写
results = await asyncio.gather(...)然后直接遍历——必须显式保存每个Task对象,再逐个.result() - 如果某个子任务失败,
task.result()会抛出该异常(不是打包进列表),所以你得在async with外层用except*或在每个.result()调用点单独捕获 - 想按 ID 区分结果?得在创建时传参:
tg.create_task(fetch_user(1), name="user-1"),然后靠task.get_name()匹配,而不是依赖位置索引
TaskGroup 的价值不在“快”,而在“收得干净”。它不帮你省时间,但能让你在凌晨三点收到告警时,一眼看出是哪个请求炸了、哪些连接没关、有没有锁没释放——而不是对着监控图表猜谜。**Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











