Python Asyncio疑问:为何需3次await才能完成任务?
问题场景
存在一个仅执行0.1秒sleep的协程,main函数为该协程创建任务,通过await asyncio.sleep(0)启动它,随后自身调用time.sleep(0.5)(时长大于协程sleep时间)。运行后出现两个疑问:
- 为何main函数唤醒后,协程仍未完成?
- 为何恰好需要3次
await才能让协程完成?(测试多个协程时仍需3次)
测试代码
import asyncio import time async def coro(): print(' coro started to sleep for 0.1 second') await asyncio.sleep(0.1) print(' [x] coro finished') async def main(): coro_task = asyncio.create_task(coro()) await asyncio.sleep(0) time.sleep(0.5) i = 0 while not coro_task.done(): i += 1 print(i) await asyncio.sleep(0) asyncio.run(main())
运行输出
coro started to sleep for 0.1 second 1 2 3 [x] coro finished
补充说明:若将time.sleep设为0.1,协程完成前会有数千次循环;time.sleep时长越长,循环次数越少;时长超过协程sleep时间后,循环次数固定为3。
疑问解答
疑问1:为何main函数唤醒后,协程仍未完成?
这是因为time.sleep(0.5)是阻塞式的同步调用,会直接占用当前线程,完全卡死asyncio的事件循环。
当你调用await asyncio.sleep(0)时,事件循环切换到coro执行,coro遇到await asyncio.sleep(0.1)后挂起,告诉事件循环“0.1秒后唤醒我”。但事件循环还没来得及处理这个定时器任务,main就调用了time.sleep(0.5)——这会让整个线程停摆,事件循环根本无法运行,自然没法检查coro的定时器是否到期,更没法唤醒coro继续执行。
等time.sleep(0.5)结束,main回到事件循环时,coro的定时器其实早就到期了,但事件循环必须等到下一次调度点(也就是你调用await asyncio.sleep(0)时)才会处理到期任务,所以此时coro仍处于未完成状态。
疑问2:为何恰好需要3次await才能让协程完成?
这和asyncio事件循环的调度逻辑以及定时器处理流程直接相关:
- 第一次
await asyncio.sleep(0):事件循环从main切出,开始处理到期的定时器任务,但仅把coro的唤醒请求加入待处理队列,不会立即执行coro剩余代码。 - 第二次
await asyncio.sleep(0):事件循环处理队列任务,将coro标记为可运行,但仍需等到下一次调度才能让coro获得执行权。 - 第三次
await asyncio.sleep(0):coro终于拿到执行权,完成剩余代码、打印结束信息,任务被标记为done。
当time.sleep时长超过coro的sleep时间后,coro的定时器早已到期,事件循环需要走完“发现到期任务→标记可运行→执行任务”这三次调度流程,所以固定需要3次await。
而当time.sleep时长较短(比如0.1秒)时,coro的定时器可能还没到期,事件循环每次调度都要检查定时器状态,直到定时器真正到期后才会进入上述三次流程,因此循环次数会非常多。
内容的提问来源于stack exchange,提问作者Igor

