被 `asyncio.gather` 和 `wait` 坑惨了:异常处理的天壤之别

简介: 本文分享一次爬虫开发中因`asyncio.gather`异常机制引发的“崩溃周末”:默认“一损俱损”,导致单个API超时就中断全部任务。详解`return_exceptions=True`容错方案与更灵活的`asyncio.wait`(支持`FIRST_EXCEPTION`等精细控制),助你按需选择并发策略。

一个让人崩溃的周末

先说说我是怎么踩到这个坑的。

上周末,我写了一个爬虫脚本,要同时从三个不同的 API 接口拉数据。逻辑很简单——三个接口互不依赖,谁先返回都行,我只要把结果汇总到一起。

一开始我用的是 asyncio.gather,代码大概是这样的:

async def fetch_user_info():
   await asyncio.sleep(1)
   return {"name": "张三"}

async def fetch_order_history():
   await asyncio.sleep(2)
   return {"orders": [1, 2, 3]}

async def fetch_recommendations():
   await asyncio.sleep(3)
   return {"recs": ["商品A", "商品B"]}

async def main():
   results = await asyncio.gather(
       fetch_user_info(),
       fetch_order_history(),
       fetch_recommendations()
   )
   print(results)

跑起来很顺利,三个任务并发执行,总耗时 3 秒,比串行的 6 秒快了一倍。

然后问题来了——第二个接口偶尔会超时抛异常。按我的理解,gather 应该会把这个异常捕获住,然后我统一处理就行了。结果实际情况是:只要有一个任务抛出异常,整个 gather 直接炸掉,其他两个任务也被取消,什么都没拿到

三个接口,一个出问题,全盘皆输。这显然不是我想要的。

当时我脑子里只有一个念头:这玩意儿怎么这么不智能?

后来我才发现,不是 gather 不智能,是我压根没搞清楚它的异常处理逻辑。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (61).png

gather 的默认行为:一损俱损

asyncio.gather 的设计哲学是 “原子性” ——它把传进去的一堆任务当作一个不可分割的整体。要么全部成功,要么全部失败。只要有一个任务抛出异常,gather 会立即把这个异常抛给调用者,同时取消其他所有未完成的任务。

这个设计在有些场景下是合理的。比如你要批量更新数据库,必须保证所有更新都成功才算完成,任何一个失败整个操作都应该回滚。这时候 gather 的“一损俱损”恰恰是你需要的。

但在我这个爬虫场景里,三个接口互不依赖,一个接口挂了不应该影响另外两个。我需要的是 “各自安好” ,而不是“连坐”。

return_exceptions:gather 的“容错开关”

后来翻文档才发现,gather 其实提供了一个参数叫 return_exceptions,专门用来控制异常处理行为。

默认是 False,也就是“一有异常就抛出”。改成 True 之后,行为完全不一样了:

results = await asyncio.gather(
   fetch_user_info(),
   fetch_order_history(),  # 这个会抛异常
   fetch_recommendations(),
   return_exceptions=True   # 关键在这里
)

for result in results:
   if isinstance(result, Exception):
       print(f"捕获到异常: {result}")
   else:
       print(f"正常结果: {result}")

设置 return_exceptions=True 之后,gather 不会抛出异常,而是把异常对象当作普通结果一起放到返回列表里。这样所有任务都会执行完,成功的有结果,失败的返回异常对象,你自己来判断和处理。

这个方案完美解决了我的问题——接口 2 超时了,接口 1 和接口 3 的数据照样能拿到。

wait:更底层的“监控器”

除了 gatherasyncio 还提供了一个更底层的 API 叫 asyncio.wait

如果说 gather 是一个“任务聚合器”,那 wait 更像一个 “状态监控器” 。它不直接返回结果,而是返回两个集合:done(已完成的任务)和 pending(未完成的任务)。

done, pending = await asyncio.wait([
   fetch_user_info(),
   fetch_order_history(),
   fetch_recommendations()
])

for task in done:
   try:
       result = task.result()
       print(f"成功: {result}")
   except Exception as e:
       print(f"任务失败了: {e}")

gather 不同,wait 默认不会自动传播异常。任务抛了异常就抛了,wait 只管告诉你“这个任务做完了”,至于结果是成功还是异常,你得自己调用 task.result() 去拿——如果是异常,result() 会重新抛出。

换句话说,gather 默认帮你“代劳”了异常处理(直接抛给你),而 wait 把异常“藏”在任务里,让你自己去取。前者是主动出击,后者是你主动去问

wait 的进阶玩法:FIRST_EXCEPTION

waitgather 强大的地方在于,它可以通过 return_when 参数精确控制“什么时候返回”。

默认是 ALL_COMPLETED——等所有任务都完成。但你还可以选:

  • FIRST_COMPLETED:任意一个任务完成就返回
  • FIRST_EXCEPTION:任意一个任务抛出异常就返回

FIRST_EXCEPTION 特别有用。假设你同时发起多个请求,只要有一个失败了你就想立刻停止并处理,不需要等其他的。用 wait 可以这样写:

done, pending = await asyncio.wait(
   tasks,
   return_when=asyncio.FIRST_EXCEPTION
)

# 检查 done 里有没有异常
for task in done:
   if task.exception():
       # 有一个任务失败了,取消所有 pending 的任务
       for p in pending:
           p.cancel()
       raise task.exception()

这种精细控制是 gather 做不到的。gatherreturn_exceptions=True 虽然能容错,但无法实现“一有异常就立刻中断并返回”的逻辑——它要么全等(默认),要么全等但返回异常对象(return_exceptions=True)。

一张表说清楚

对比维度 asyncio.gather asyncio.wait
返回内容 结果列表(按输入顺序) done 和 pending 两个任务集合
默认异常处理 自动传播第一个异常,取消其他任务 不自动传播,需手动检查
容错模式 return_exceptions=True 将异常作为结果返回 手动遍历 done 集合,调用 task.exception()
返回时机控制 全部完成(或遇到异常提前中断) ALL_COMPLETED / FIRST_COMPLETED / FIRST_EXCEPTION
适用场景 批量获取结果、事务一致性要求高的场景 任务监控、动态调度、需要精细控制的场景

什么时候该用谁?

经过这次踩坑,我给自己定了一个简单的原则:

如果你要的是“全部结果,按顺序拿”,用 gather 它简单、直接,配合 return_exceptions=True 也能优雅地处理异常。

如果你需要“监控任务状态、根据情况决定下一步”,用 wait 它更灵活,能告诉你哪些做完了、哪些还在跑、哪个先出了异常。

**如果异常发生时你想立刻停止所有任务并处理,用 wait 配合 FIRST_EXCEPTION**。

**如果某个任务失败了你不想影响其他任务,用 gather 配合 return_exceptions=True**。

没有哪个绝对好,只有哪个更适合你的场景。

写在最后

那次爬虫事件之后,我每次用 asyncio.gather 都会下意识地看一眼需不需要加 return_exceptions=True。这个习惯帮我避免了好几次线上事故。

其实 Python 的异步编程就是这样——API 看起来差不多,用起来天差地别。gatherwait 的区别只是冰山一角,类似的坑还有 create_taskensure_futureshieldwait_for……

每一个坑踩过一次,下次就知道了。

希望你不用踩同样的坑。

目录
相关文章
|
5天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1568 111
|
12天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1939 8
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
6天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
|
6天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
526 112
|
18天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
2551 4
|
10天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
720 111
|
20天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2634 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
6天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
7天前
Qoder 一周年 × Qwen3.8-Max 正式上线,多重好礼限时领
8月3日,Qwen3.8-Max 正式上线Qoder,迎来Qoder一周年。新老用户可领800次免费调用,下单再赠2000次;夜间(22:00–08:00)调用5折;邀请好友双方得积分与调用额度。
443 1