查看: 198|回复: 0

Python asyncio.gather 异常取消与并发限流实战

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
Python 的 asyncio.gather 是异步并发中常用的结果聚合器,适合同时请求多个 API、并行查询数据库、批量下载等需要等待全部结果再统一处理的场景。它接收多个 awaitable,并按输入顺序返回结果。本文围绕它的执行流程、异常与取消、并发限制、常见坑和落地封装展开。

一、基本用法与三个核心对象
asyncio.gather 的基本调用形式如下:
  1. results = await asyncio.gather(task1, task2, task3)
复制代码
参数可以是协程对象、Task 对象或 Future 对象。传入协程时,gather 内部会用 ensure_future() 将其转换为 Task 并调度执行;传入 Future/Task 则直接使用。你不需要手动 create_task,gather 会处理包装。需要真正理解 gather,先分清 Coroutine、Task 和 Future:
  1. async def fetch_data():
  2.     return 'data'
  3. coro = fetch_data()  # 协程对象,尚未执行
  4. task = asyncio.create_task(fetch_data())  # Task 包装并调度执行
  5. future = asyncio.Future()
  6. future.set_result('done')  # 手动设置结果
复制代码
Coroutine 由 async def 定义,调用后返回协程对象,本身不会自动执行;Task 是 Future 的子类,用于包装协程并交给事件循环尽快执行;Future 表示异步操作的最终结果,状态从 pending 变为 finished 或 cancelled。

二、gather 的生命周期
调用 asyncio.gather(*aws) 后,内部先遍历所有输入参数,对每个参数调用 ensure_future():协程转为 Task,Future/Task 直接使用。接着检查所有 Future 是否属于同一事件循环,并创建 _GatheringFuture 作为聚合器。gather 还会对相同 Future 去重:同一个 Future 传入两次只会被调度一次,但结果会在返回列表中重复出现。

执行阶段,gather 为每个子任务注册完成回调。回调负责计数已完成任务、根据 return_exceptions 判断异常处理方式,并在所有任务完成后汇总结果。结果顺序严格保持与输入参数一致。例如:
  1. async def slow(n):
  2.     await asyncio.sleep(n)
  3.     return n
  4. results = await asyncio.gather(slow(1), slow(0.5))
  5. print(results)  # [1, 0.5]
复制代码
即使 slow(0.5) 先完成,结果列表仍然按输入顺序排列。完成阶段,gather 按原始顺序收集结果,并处理取消和异常:外层被取消时,所有子任务会被取消;子任务抛异常时,是否立即传播由 return_exceptions 决定。

三、异常处理:return_exceptions 的两种行为
默认 return_exceptions=False。任一子任务异常会立即传播到聚合器,其他子任务不会被自动取消,仍可能继续执行。例如:
  1. async def bad():
  2.     raise ValueError('oops')
  3. try:
  4.     await asyncio.gather(good(), bad())
  5. except ValueError as e:
  6.     print(f'捕获到异常: {e}')  # good() 可能仍在执行
复制代码
当 return_exceptions=True 时,所有异常会作为正常结果收集,返回列表中包含异常对象而不是引发异常:
  1. results = await asyncio.gather(good(), bad(), return_exceptions=True)
  2. # results 可能是 ['good', ValueError('oops')]
复制代码
需要注意,即使使用 return_exceptions=True,如果聚合器本身被取消,取消请求仍会传播,并最终抛出 CancelledError。

四、取消传播与子任务取消
取消 gather 返回的聚合 Future 时,取消请求会传播到所有子任务。聚合器会等待所有子任务完成或取消,最后以 CancelledError 结束:
  1. async def worker():
  2.     try:
  3.         await asyncio.sleep(10)
  4.     except asyncio.CancelledError:
  5.         print('我被取消了')
  6.         raise
  7. async def main():
  8.     gather_task = asyncio.create_task(asyncio.gather(worker(), worker()))
  9.     await asyncio.sleep(0.1)
  10.     gather_task.cancel()
  11.     try:
  12.         await gather_task
  13.     except asyncio.CancelledError:
  14.         print('聚合器被取消')
复制代码
单个子任务被取消时,该任务以 CancelledError 结束。return_exceptions=False 时聚合器立即抛出 CancelledError;return_exceptions=True 时,结果列表对应位置是 CancelledError 对象。

五、并发限制、内存控制与 Semaphore
gather 本身不限制并发量。一次性创建大量任务可能造成内存压力,因此不建议对成千上万的 URL 直接全部 gather。可以分批处理:
  1. BATCH_SIZE = 100
  2. for i in range(0, len(urls), BATCH_SIZE):
  3.     batch = urls[i:i + BATCH_SIZE]
  4.     await asyncio.gather(*[download(url) for url in batch])
复制代码
如果并发量需要更精细控制,可以配合 asyncio.Semaphore:
  1. sem = asyncio.Semaphore(10)
  2. async def limited_download(url):
  3.     async with sem:
  4.         return await download(url)
  5. await asyncio.gather(*[limited_download(url) for url in urls])
复制代码
Python 3.11+ 提供了 asyncio.TaskGroup,结构化并发更安全。TaskGroup 在任一任务失败时会自动取消其他任务,异常总是传播;结果收集需要单独处理。gather 则兼容所有版本,结果顺序有保障,异常传播和取消行为更依赖手动控制。

六、常见问题与 safe_gather 封装
gather 默认不会因为一个任务失败而取消其他任务,若只捕获异常而不处理剩余任务,可能导致资源泄漏或静默失败。一个常见的安全封装是:创建任务列表,捕获异常后取消所有未完成任务,并等待它们结束:
  1. async def safe_gather(*coros):
  2.     tasks = [asyncio.create_task(c) for c in coros]
  3.     try:
  4.         return await asyncio.gather(*tasks)
  5.     except Exception as e:
  6.         for t in tasks:
  7.             t.cancel()
  8.         await asyncio.gather(*tasks, return_exceptions=True)
  9.         raise e
复制代码
另一个常见误区是把结果处理写成串行。gather 负责并发调度并等待全部完成,后面的 for 循环只是顺序处理结果;如果处理函数本身包含 await 且耗时,需要另外设计并发处理或使用 as_completed。调试时可以包装每个任务,记录开始、完成和失败时间:
  1. async def traced(task):
  2.     print(f'开始 {task}')
  3.     try:
  4.         result = await task
  5.         print(f'完成 {task}')
  6.         return result
  7.     except Exception as e:
  8.         print(f'失败 {task}: {e}')
  9.         raise
  10. await asyncio.gather(traced(task1()), traced(task2()))
复制代码

七、超时、部分成功与进度反馈
结合 asyncio.wait_for 可以实现整体超时:
  1. try:
  2.     await asyncio.wait_for(
  3.         asyncio.gather(task1(), task2()),
  4.         timeout=5.0
  5.     )
  6. except asyncio.TimeoutError:
  7.     print('整体操作超时')
复制代码
只需要部分成功时,可以用 return_exceptions=True 后过滤异常对象:
  1. results = await asyncio.gather(*tasks, return_exceptions=True)
  2. success = [r for r in results if not isinstance(r, Exception)]
  3. if len(success) >= MIN_REQUIRED:
  4.     process(success)
复制代码
进度反馈可以通过包装任务和计数器实现:
  1. done = 0
  2. async def tracked(coro, total):
  3.     global done
  4.     result = await coro
  5.     done += 1
  6.     print(f'{done}/{total} 完成')
  7.     return result
  8. tasks = [tracked(task(i), len(urls)) for i in range(len(urls))]
  9. await asyncio.gather(*tasks)
复制代码

八、内部实现与并发模式对比
gather 返回的聚合器由 _GatheringFuture 实现。它扩展标准 Future,重写 cancel() 方法以支持级联取消,维护子任务列表和完成状态,并处理异常传播与结果收集。每个子任务完成时会触发回调,回调检查全局状态,决定立即传播异常还是等待其他任务。CPython 实现中还有一些优化:使用 weakref 避免循环引用、为同步完成的任务走快速路径、尽量减少回调函数内存占用。

与其他并发模式相比:
as_completed 更适合流式处理,先完成的任务可以先处理:
  1. for fut in asyncio.as_completed(tasks):
  2.     result = await fut
  3.     process_immediately(result)
复制代码
wait 提供更细粒度控制,可以指定 FIRST_COMPLETED、FIRST_EXCEPTION 等完成策略:
  1. done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
复制代码
简单并发且需要全部结果时,gather 接口简单、顺序有保障;需要结构化并发时优先 TaskGroup;需要流式处理时用 as_completed;需要细粒度控制时用 wait;需要限制并发时用 gather 加 Semaphore。

九、增强版 safe_gather 与决策建议
原文给出的实战增强版封装综合了并发控制、全局超时、错误处理和资源清理:
  1. async def safe_gather(*coros, timeout=None, max_concurrent=100):
  2.     sem = asyncio.Semaphore(max_concurrent)
  3.     async def limited(coro):
  4.         async with sem:
  5.             return await coro
  6.     tasks = [asyncio.create_task(limited(coro)) for coro in coros]
  7.     try:
  8.         return await asyncio.wait_for(asyncio.gather(*tasks), timeout=timeout)
  9.     except Exception as e:
  10.         for t in tasks:
  11.             t.cancel()
  12.         await asyncio.gather(*tasks, return_exceptions=True)
  13.         raise e
复制代码
实际项目中可遵循几条经验:监控任务数量,避免一次性创建太多任务;给整体流程设置合理超时,防止慢任务拖住全局;任务失败后清理未完成协程,避免资源泄漏;关键任务单独处理,避免被其他任务影响。掌握这些细节后,asyncio.gather 的价值就在于简单、结果顺序稳定;复杂场景则交给 TaskGroup 等更结构化的工具。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-22 12:35 , Processed in 0.021632 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部