事件循环在做什么
事件循环可以先想成一个值班员。它手里有几张清单:现在能做的事、过一会儿才能做的事、等 socket 有动静才能做的事。它一轮一轮检查这些清单,能做的就拿出来执行。
先看最小版本
一个事件循环至少需要三样东西:
| 组件 | 作用 |
|---|---|
| ready 队列 | 现在就能执行的 callback |
| scheduled 堆 | 到点以后才执行的 timer callback |
| I/O 多路复用器 | 等 socket、pipe 变成可读或可写 |
循环每一轮大概是:
CPython 里的 _run_once() 要处理更多细节,但你先把这一轮看懂就够了:到点的 timer 进 ready;I/O 有动静的 callback 进 ready;最后运行 ready 里的 callback。
asyncio.run 做了什么
应用入口一般写:
asyncio.run(main())它会做这些收尾工作:
- 创建一个新的事件循环。
- 把传入的 awaitable 包装并运行到结束。
- 处理异步生成器收尾。
- 关闭默认 executor。
- 关闭事件循环。
日常代码里,把 asyncio.run() 当成程序入口用就好,不要在程序中间反复开关事件循环。Jupyter、IPython、Web 框架这类环境通常已经有事件循环,里面一般直接 await main() 或按框架要求接入。
running loop 和 current loop
在协程里需要拿当前事件循环时,优先用:
loop = asyncio.get_running_loop()它只在当前线程确实有事件循环正在运行时成功。相比 get_event_loop(),这个判断更干脆,不容易被历史兼容行为绕晕。
这套教程以 get_running_loop() 为主,不把 get_event_loop() 当入口讲。后者带着较多历史兼容行为,读源码时会见到,但写新代码时不必优先选它。
selector 有什么用
没有 selector,事件循环只能忙等:
while True:
check_every_socket()这会浪费 CPU。selector 的作用是把等待交给操作系统:有 socket 可读或可写时,再叫醒事件循环。
在 Unix 上,默认事件循环基于 selector。Windows 上还有 ProactorEventLoop 这类不同实现。应用层多数时候不需要关心差异,但读源码时要知道平台相关代码存在。
call_soon 和 call_later
call_soon(callback, *args) 把 callback 放进 ready 队列。
call_later(delay, callback, *args) 把 callback 放进定时器堆,到期后再进入 ready 队列。
asyncio.sleep(delay) 不是让线程睡着。它会创建一个 Future,再安排一个 timer:delay 到了以后,把这个 Future 标记为完成。正在 await 它的 Task 就能继续往下走。
为什么一个慢 callback 会堵住所有任务
事件循环同一时刻只运行一个 callback 或一个 Task 的一小步。假设某个 callback 连续算了 300ms:
这 300ms 里,别的 Task 没机会运行,socket 就绪事件也没机会处理。debug 模式记录慢 callback,就是为了抓这种问题。
小练习
阅读 examples/mini_asyncio_runtime/mini_asyncio/core.py 的 EventLoop._run_once(),对照本章流程图标出三段逻辑:
- timer 到期处理。
- selector 等待 I/O。
- ready callback 执行。