为何Asyncio事件循环的迭代次数超出预期?
你的理解偏差:事件循环的
_run_once是完整轮询周期,而非协程切换次数 你预期的2次是协程的“执行-暂停”和“恢复-结束”两次切换,但事件循环的_run_once方法对应的是一次完整的轮询处理周期,这个周期包含多个步骤,远不止协程的单次执行片段。结合CPython 3.11.4的事件循环逻辑,四次_run_once调用的来源如下:
四次迭代的具体流程
假设你的测试代码是类似这样的标准协程:
import asyncio async def test_coro(): await asyncio.sleep(0.1) asyncio.run(test_coro())
四次_run_once的触发场景分别是:
第一次迭代:
事件循环启动后,首先调度初始协程任务,执行test_coro直到await asyncio.sleep(0.1)。此时协程挂起,sleep会注册一个定时器回调到事件循环,本次轮询周期结束。第二次迭代:
事件循环没有就绪的任务或IO事件,进入IO等待(比如epoll_wait),直到定时器到期。到期后,事件循环处理定时器回调,将sleep对应的Future标记为完成,并把协程重新加入就绪任务队列,本次轮询周期结束。第三次迭代:
事件循环发现就绪的协程任务,调度并执行它,直到协程完全结束。此时任务标记为完成,但事件循环还需要确认是否有其他待处理的任务或回调,本次轮询周期结束。第四次迭代:
事件循环检查所有任务和回调,确认没有未完成的工作,触发退出前的最后一次轮询(包括清理内部状态、检查是否有遗留回调等),随后事件循环终止。
核心偏差点
你混淆了协程的执行阶段数和事件循环的轮询周期数:
- 协程确实只有两次主要执行阶段(到
await暂停、唤醒后结束),但这两个阶段并不直接对应两次_run_once。 - 每次
_run_once会完成一整套流程:处理就绪回调、等待IO/定时器、处理到期事件、调度新任务,这些步骤可能跨越协程的多次状态切换。
事件循环的设计目标是一次轮询处理所有当前就绪的事件,而不是为每个协程切换单独启动一轮轮询,这也是为什么你会看到比预期更多的_run_once调用次数。
内容的提问来源于stack exchange,提问作者Andres Ariza.
相关产品推荐
相关产品推荐

