asyncio嵌套协程的事件循环处理及执行确定性等问题咨询
import asyncio import time async def g(i): print("sleeping start", i) time.sleep(2) print("sleeping end", i) async def task(i): print("start task", i) await g(i) print("end task", i) async def f(i): print("start", i) await task(i) print("status update",i) print("end", i) async def main(): await asyncio.gather(*[f(i) for i in range(5)]) asyncio.run(main())
我有一系列嵌套协程,通过asyncio.gather调用作为统一入口触发所有协程,有以下问题:
- 在这类设置中,输出是否具有确定性?即运行前能否预知打印顺序?
- 当调用嵌套协程(例如
await g(1))时,事件循环是否会将其入队,转而运行f(2)?
最后一个跟进问题:如果我必须确保输出顺序如下:
start 0 start task 0 sleeping start 0 sleeping end 0 end task 0 status update 0 end 0 ...
是否有办法实现这一点?
我的核心目标是:当包含任务的协程完成(即输出sleeping end i)时,父协程能立即执行到status update i,不希望在此期间出现延迟或事件循环切换到其他任务。
问题1:输出是否具有确定性?
在当前代码里,输出完全确定,是严格的串行执行顺序。原因是g(i)中使用的time.sleep(2)是同步阻塞函数,会直接卡住整个事件循环,没有任何协程切换的机会。事件循环会先把f(0)从头到尾执行完毕(包括等待2秒),再执行f(1),以此类推,打印顺序完全固定。
但如果把time.sleep(2)替换为await asyncio.sleep(2)(异步睡眠),输出就不再确定。因为await会触发协程切换,当某个协程进入睡眠时,事件循环会立即调度其他就绪的协程执行,打印顺序会变成交错状态(比如先打印所有start i,再陆续打印sleeping end i等)。
问题2:事件循环是否会切换到其他协程?
在当前代码里,不会切换。因为g(i)里的time.sleep是同步阻塞操作,一旦执行到这里,整个程序都会卡住,事件循环根本没有机会调度其他协程。
如果换成await asyncio.sleep(2),当执行到await g(1)时,g(1)会在await asyncio.sleep(2)处让出控制权,事件循环会立即切换到其他已经就绪的协程(比如f(2))执行,直到g(1)的睡眠完成后才会继续后续代码。
跟进问题:如何保证串行完整的输出顺序?
要实现你想要的顺序,核心是让每个f(i)的完整流程执行完毕后,再启动下一个协程,有两种可行方式:
方式1:放弃并发,串行执行协程
直接在main里逐个await每个f(i),替代asyncio.gather的并发启动:
async def main(): for i in range(5): await f(i)
这样每个f(i)会从头到尾完整执行,不会被其他协程打断,完全符合你要求的输出顺序。
方式2:保留gather但锁定收尾代码
如果一定要用asyncio.gather,同时保证sleeping end i之后的代码不被打断,可以用异步锁锁定这部分逻辑,确保独占执行:
import asyncio import time lock = asyncio.Lock() async def g(i): print("sleeping start", i) await asyncio.sleep(2) # 使用异步睡眠 print("sleeping end", i) async def task(i): print("start task", i) await g(i) print("end task", i) async def f(i): print("start", i) await task(i) async with lock: # 这段代码会独占执行,不会被其他协程插入 print("status update",i) print("end", i) async def main(): await asyncio.gather(*[f(i) for i in range(5)]) asyncio.run(main())
这种方式下,协程会在异步睡眠时切换,但sleeping end i之后的收尾代码会通过锁保证连续执行。不过整体打印顺序仍会有交错(比如多个start i和sleeping start i会先输出),如果要完全严格的串行顺序,方式1是最直接的选择。
内容的提问来源于stack exchange,提问作者Utsav Dutta

