TaskGroup 是 Python 3.11 引入的结构化并发机制,用于统一启动、等待和异常传播的任务组管理;它不提供数据同步能力,共享状态仍需 asyncio.Queue、Lock 等原语显式保护。

TaskGroup 不能直接“管理并发异步数据结构”,它只负责**统一启动、等待和传播异常的异步任务组**;你要操作的数据结构(比如 asyncio.Queue、asyncio.Lock 或自定义协程安全容器)仍需单独创建和显式使用。
TaskGroup 的核心职责是任务生命周期协同,不是数据同步
asyncio.TaskGroup 在 Python 3.11 引入,本质是 async with 封装的结构化并发控制机制。它不提供线程/协程安全的数据结构,也不自动保护共享变量。
- 常见误用:以为
async with asyncio.TaskGroup() as tg:后,对list或dict的读写就自动线程安全了 —— 实际完全不安全,会引发竞态 - 正确分工:
TaskGroup负责“哪些协程一起跑、一起停、一个挂全挂”;数据同步靠asyncio.Queue、asyncio.Lock、asyncio.Condition等原语 - 典型场景:并发抓取多个 URL 并汇总结果 → 用
TaskGroup启动所有抓取协程,用asyncio.Queue收集响应,用asyncio.Lock保护写入共享list
如何配合 asyncio.Queue 实现安全的结果收集
asyncio.Queue 是为协程设计的线程/并发安全队列,天然适配 TaskGroup 中的任务协作。
- 不要在多个任务里直接 append 到同一个普通
list,这会丢失数据或引发异常 - 创建
queue = asyncio.Queue()在TaskGroup外围,所有子任务通过await queue.put(...)写入 - 主协程可在
TaskGroup结束后用while not queue.empty(): await queue.get()拉取全部结果(注意:需配合queue.qsize()或计数器判断完成,因为empty()在并发下不可靠) - 示例关键片段:
async def fetch_and_put(url, queue): data = await aiohttp_get(url) await queue.put(data) # 安全写入 <p>async def main(): queue = asyncio.Queue() async with asyncio.TaskGroup() as tg: for url in urls: tg.create_task(fetch_and_put(url, queue))</p><h1>所有任务结束,开始消费</h1><pre class='brush:python;toolbar:false;'>results = [] while not queue.empty(): results.append(await queue.get())</pre></li>
何时必须加 asyncio.Lock?什么时候可以省略?
对非原子、非线程安全的对象(如普通 dict、list、自定义计数器)做读-改-写操作时,Lock 不可省略;仅读取或仅写入单个原子值(如赋值 result = value)通常不需要。
立即学习“Python免费学习笔记(深入)”;
- 需要锁的典型操作:
shared_dict[url] = shared_dict.get(url, 0) + 1(读+算+写三步,非原子) - 不需要锁的场景:
results.append(data)看似简单,但在多协程下仍不安全 ——list.append不是协程安全的,必须加锁或换用Queue - 性能提示:锁会串行化执行,若热点路径频繁争用,优先考虑无锁结构(如
Queue、分片字典、消息传递) - 错误示范:
counter = 0 async def inc(): global counter counter += 1 # 危险!竞态导致计数丢失正确做法是用asyncio.Lock包裹该行,或改用asyncio.Queue发送增量信号
真正容易被忽略的是:即使你用了 TaskGroup,只要没显式处理共享状态的并发访问,程序就可能在高负载下间歇性出错——这种 bug 很难复现,但上线后会在流量高峰突然爆发。


















