asyncio 事件循环是什么?它在异步编程中负责什么?
简化版
事件循环是 asyncio 的调度核心,负责运行协程、调度 Task、监听 I/O、处理定时器和回调。可以把它理解成一个不断循环的调度器:谁准备好了就运行谁,谁在等待 I/O 就先挂起。
详细版
常见入口:
import asyncio
async def main():
await asyncio.sleep(1)
asyncio.run(main())
asyncio.run() 会创建事件循环,运行入口协程,结束后清理事件循环资源。
事件循环负责:
- 驱动协程继续执行;
- 管理
Task的状态; - 处理
await asyncio.sleep()这类定时器; - 监听 socket 等异步 I/O;
- 执行回调;
- 协调取消、异常、任务收尾。
面试中可以这样回答:事件循环是单线程异步并发的“调度中心”,协程本身不会自己跑,必须由事件循环驱动。
完整版教学
一、协程为什么需要事件循环
协程对象本身只是一个可暂停、可恢复的执行对象。它不会自动执行,也不会自己决定什么时候恢复。
事件循环负责做这件事:
检查哪些任务可以运行 → 运行到下一个 await → 记录等待条件 → 继续调度其他任务
所以 asyncio 程序通常需要一个入口:
asyncio.run(main())
没有事件循环,协程就像排队等开工的任务单。
二、事件循环如何实现并发
假设有三个请求任务:
async def fetch(name):
await asyncio.sleep(1)
return name
事件循环运行第一个任务,遇到 await asyncio.sleep(1),发现它要等 1 秒,于是挂起它,去运行第二个任务。第二个任务也等待,再去运行第三个。
1 秒后,这些任务陆续变成可运行状态,事件循环再恢复它们。
整个过程中不需要为每个任务创建一个线程。
三、asyncio.run 做了什么
日常代码推荐用:
asyncio.run(main())
它大致负责:
- 创建新的事件循环;
- 设置为当前运行环境的循环;
- 运行入口协程;
- 等待异步生成器和默认 executor 收尾;
- 关闭事件循环。
在普通脚本里,不建议自己手动反复创建和关闭 loop,容易出现资源清理问题。
四、事件循环不是多线程调度器
默认情况下,一个事件循环通常运行在一个线程里。同一时刻只有一个协程片段在执行。异步并发依赖的是协程在等待点让出执行权,而不是多个协程同时在多个 CPU 核上跑。
这意味着:
- CPU 密集计算会卡住事件循环;
- 阻塞函数会卡住事件循环;
- 协程代码中长时间不
await,其他任务就没机会运行。
async def bad():
while True:
heavy_compute()
这种代码会让事件循环失去调度机会。
五、事件循环和 Web 框架的关系
FastAPI、aiohttp、Sanic 等异步 Web 框架底层都会依赖事件循环。请求进来后,框架把请求处理逻辑作为协程任务调度。
如果你的 handler 里使用异步数据库客户端、异步 HTTP 客户端,就能在等待 I/O 时释放事件循环去处理其他请求。
如果 handler 里直接使用阻塞数据库驱动或 requests.get(),事件循环就会被卡住,高并发优势会明显下降。
六、常见误区与追问
| 概念 | 作用 | 面试判断 |
|---|---|---|
| 协程对象 | 表示一段可暂停的异步逻辑 | 创建后不自动运行 |
| Task | 把协程注册到事件循环 | 才能被调度并发推进 |
| 事件循环 | 轮询就绪任务和 I/O 事件 | 单线程内协作式调度 |
asyncio.run | 创建、运行、关闭事件循环 | 应作为异步程序入口使用 |
- 误区:有 async def 就等于代码已经异步运行。
async def调用后得到的是协程对象,只有被await、包装成Task或交给事件循环才会执行。 - 误区:事件循环就是多线程调度器。 默认事件循环通常在一个线程里运行,依靠协程主动在
await处让出控制权实现并发。 - 误区:asyncio.run 可以在任意地方反复嵌套调用。 它会创建并管理事件循环,不能在已经运行的事件循环中随意调用,Web 框架内部通常已经有循环。
- 追问:事件循环为什么能处理很多连接? 网络 I/O 等待期间不占用当前协程执行权,循环可以切换去推进其他已就绪任务。
- 追问:一个阻塞函数为什么会拖垮所有协程? 如果它长时间不
await且占住事件循环线程,其他协程就没有机会被调度。 - 追问:事件循环和 uvicorn、FastAPI 有什么关系? ASGI 服务器负责启动和管理事件循环,应用中的异步视图运行在这个循环之上。
记忆钩子:事件循环不是把 Python 变成多线程,而是在一个线程里安排“谁等 I/O,谁先继续跑”。
七、加强记忆
事件循环是 asyncio 的调度中枢:协程负责“能暂停”,Task 负责“可调度”,事件循环负责“什么时候继续跑”。它通常在单线程里通过 await 点切换任务,所以异步适合 I/O 并发,不适合把 CPU 重活直接塞进 loop。