一个让人崩溃的周末先说说我是怎么踩到这个坑的。上周末我写了一个爬虫脚本要同时从三个不同的 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不智能是我压根没搞清楚它的异常处理逻辑。gather 的默认行为一损俱损asyncio.gather的设计哲学是“原子性”——它把传进去的一堆任务当作一个不可分割的整体。要么全部成功要么全部失败。只要有一个任务抛出异常gather会立即把这个异常抛给调用者同时取消其他所有未完成的任务。这个设计在有些场景下是合理的。比如你要批量更新数据库必须保证所有更新都成功才算完成任何一个失败整个操作都应该回滚。这时候gather的“一损俱损”恰恰是你需要的。但在我这个爬虫场景里三个接口互不依赖一个接口挂了不应该影响另外两个。我需要的是“各自安好”而不是“连坐”。return_exceptionsgather 的“容错开关”后来翻文档才发现gather其实提供了一个参数叫return_exceptions专门用来控制异常处理行为。默认是False也就是“一有异常就抛出”。改成True之后行为完全不一样了results await asyncio.gather( fetch_user_info(), fetch_order_history(), # 这个会抛异常 fetch_recommendations(), return_exceptionsTrue # 关键在这里 ) for result in results: if isinstance(result, Exception): print(f捕获到异常: {result}) else: print(f正常结果: {result})设置return_exceptionsTrue之后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_EXCEPTIONwait比gather强大的地方在于它可以通过return_when参数精确控制“什么时候返回”。默认是ALL_COMPLETED——等所有任务都完成。但你还可以选FIRST_COMPLETED任意一个任务完成就返回FIRST_EXCEPTION任意一个任务抛出异常就返回FIRST_EXCEPTION特别有用。假设你同时发起多个请求只要有一个失败了你就想立刻停止并处理不需要等其他的。用wait可以这样写done, pending await asyncio.wait( tasks, return_whenasyncio.FIRST_EXCEPTION ) # 检查 done 里有没有异常 for task in done: if task.exception(): # 有一个任务失败了取消所有 pending 的任务 for p in pending: p.cancel() raise task.exception()这种精细控制是gather做不到的。gather的return_exceptionsTrue虽然能容错但无法实现“一有异常就立刻中断并返回”的逻辑——它要么全等默认要么全等但返回异常对象return_exceptionsTrue。一张表说清楚对比维度asyncio.gatherasyncio.wait返回内容结果列表按输入顺序done 和 pending 两个任务集合默认异常处理自动传播第一个异常取消其他任务不自动传播需手动检查容错模式return_exceptionsTrue将异常作为结果返回手动遍历done集合调用task.exception()返回时机控制全部完成或遇到异常提前中断ALL_COMPLETED/FIRST_COMPLETED/FIRST_EXCEPTION适用场景批量获取结果、事务一致性要求高的场景任务监控、动态调度、需要精细控制的场景什么时候该用谁经过这次踩坑我给自己定了一个简单的原则如果你要的是“全部结果按顺序拿”用gather。它简单、直接配合return_exceptionsTrue也能优雅地处理异常。如果你需要“监控任务状态、根据情况决定下一步”用wait。它更灵活能告诉你哪些做完了、哪些还在跑、哪个先出了异常。**如果异常发生时你想立刻停止所有任务并处理用wait配合FIRST_EXCEPTION**。**如果某个任务失败了你不想影响其他任务用gather配合return_exceptionsTrue**。没有哪个绝对好只有哪个更适合你的场景。写在最后那次爬虫事件之后我每次用asyncio.gather都会下意识地看一眼需不需要加return_exceptionsTrue。这个习惯帮我避免了好几次线上事故。其实 Python 的异步编程就是这样——API 看起来差不多用起来天差地别。gather和wait的区别只是冰山一角类似的坑还有create_task和ensure_future、shield和wait_for……每一个坑踩过一次下次就知道了。希望你不用踩同样的坑。