You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:23:21