关于asyncio.gather默认模式下协程未执行的疑问
我编写了如下Python代码:
import asyncio, time async def fn1(x): await asyncio.sleep(3) print(f"fn1 is called with x={x}") return "fn1 SUCCESS" async def fn2(y): await asyncio.sleep(2) print(f"fn2 is called with y={y}") raise asyncio.TimeoutError() print(y, '*'*10) return "fn2 SUCCESS" async def main(): print("start:",time.ctime()) result = ['No results'] try: result = await asyncio.gather( fn1("fn1"), fn2("fn2"), return_exceptions=False, # return_exceptions=True, ) except Exception as e: print('e:', type(e), str(e)) print("end:",time.ctime()) print(result) asyncio.run(main())
运行结果如下:
start: Mon Sep 30 17:25:28 2024 fn2 is called with y=fn2 e: <class 'TimeoutError'> end: Mon Sep 30 17:25:30 2024 ['No results']
根据Python官方文档中asyncio.gather的说明:
如果return_exceptions为False(默认值),第一个抛出的异常会立即传播至等待gather()的任务。aws序列中的其他可等待对象不会被取消,会继续运行。
但这和我的运行结果矛盾,fn1("fn1")协程并没有执行完成——如果完成的话,print(f"fn1 is called with x={x}")应该会输出内容。
后来我修改了fn1的sleep时间,让它更短:
async def fn1(x): await asyncio.sleep(3-1) print(f"fn1 is called with x={x}") return "fn1 SUCCESS"
此时fn1能执行完成,结果如下:
start: Mon Sep 30 17:30:29 2024 fn1 is called with x=fn1 fn2 is called with y=fn2 e: <class 'TimeoutError'> end: Mon Sep 30 17:30:31 2024 ['No results']
这明显和文档描述的“其他可等待对象不会被取消,会继续运行”不符,这是什么原因?
核心问题在于**asyncio.run()会在主协程结束时直接关闭事件循环**,而非等待所有后台任务完成。
当fn2在2秒后抛出异常,这个异常会立刻被gather传播给main协程,main里的except块捕获异常后,会继续执行后续的print("end:",...)和print(result),随后main协程就结束了。此时asyncio.run()会立即终止事件循环,还在sleep的fn1协程还没来得及执行到print语句就被强制终止——这并不是gather取消了fn1,而是整个事件循环被关掉了。
当你把fn1的sleep时间改成2秒后,它会在fn2抛出异常前(或同步)完成执行,所以在main协程结束前,fn1已经跑完了print语句,自然能看到输出。
验证这个逻辑很简单:在except块里加一段等待时间,给fn1足够的执行时间:
async def main(): print("start:",time.ctime()) result = ['No results'] try: result = await asyncio.gather( fn1("fn1"), fn2("fn2"), return_exceptions=False, ) except Exception as e: print('e:', type(e), str(e)) await asyncio.sleep(2) # 等待fn1完成 print("end:",time.ctime()) print(result)
运行后你会看到fn1的输出:
start: Mon Sep 30 17:25:28 2024 fn2 is called with y=fn2 e: <class 'TimeoutError'> fn1 is called with x=fn1 end: Mon Sep 30 17:25:32 2024 ['No results']
这就证明了fn1并没有被取消,只是之前事件循环在它完成前就被关闭了。文档里的描述是准确的,但要注意前提:事件循环需要保持运行,asyncio.run()的行为是主协程结束就关闭循环,不会等待后台任务。
内容的提问来源于stack exchange,提问作者CyberPlayerOne

