如果你只把
async/await当成“更快的 requests”,那你几乎肯定会在复杂场景里踩坑。
Python 协程不是语法糖,而是一套完整的用户态并发模型。
本文从 协程语义 → 事件循环 → Future / Task → selector / epoll 映射,逐层拆解 asyncio 的真实运行机制。
一、Python 协程到底是什么
一句话定义:
Python 协程是:可暂停、可恢复的函数状态机,由事件循环在用户态调度执行。
关键点:
-
不是线程
-
不会并行
-
不会被抢占
-
只能在
await处让出执行权
二、async / await 的设计思想
1. async 函数不会自动执行
async def f():
...
调用 f():
-
不会运行
-
只会返回一个 协程对象
这是刻意设计的,目的是:
-
避免“看起来同步,实际偷偷并发”
-
强制开发者显式交给事件循环调度
2. await 是唯一的切换点
await something
含义不是“等结果”,而是:
暂停当前协程,把控制权交还给事件循环
Python 明确选择:
-
协作式调度
-
显式切换点
-
可预测的执行流
这是 asyncio 能保持可读性的根本原因。
三、事件循环的本质
1. 事件循环不是魔法
事件循环本质就是一个 while 循环:
while not stopped:
run_ready_tasks()
timeout = time_to_next_timer()
events = selector.select(timeout)
schedule_io_callbacks(events)
schedule_due_timers()
它只做三件事:
-
执行就绪任务
-
等待 I/O 或时间事件
-
调度回调 / 恢复协程
2. 事件循环的核心数据结构
在 asyncio(Linux)中:
- ready queue :可立即执行的回调 / Task
- scheduled heap :sleep / timeout 定时器
- selector :I/O 多路复用(epoll)
- task / future 映射 :状态管理
事件循环本身不做 I/O,只负责调度。
四、Task 与 Future:协程真正的驱动器
1. Task 是什么
Task = Future + 协程执行逻辑
职责:
-
驱动协程运行(
coro.send()) -
在 await 处暂停
-
在 Future 完成时恢复协程
协程本身是“被动的”,真正运行靠 Task。
2. Future 是什么
Future 表示:
一个将来才会有结果的对象
它负责:
-
保存状态(pending / done / cancelled)
-
保存结果或异常
-
通知等待它的 Task
Future 是 asyncio 的“状态容器”。
五、await 的底层执行机制
result = await future
在字节码层,等价于:
yield from future.__await__()
执行流程:
-
协程运行到
await -
调用
future.__await__() -
协程 yield 控制权
-
Task 捕获 yield
-
Task 将自己注册到 future
-
协程挂起
整个过程:
-
不涉及线程
-
不涉及内核
-
完全在用户态完成
六、selector 与 epoll 的真实映射关系
1. 分层结构
协程 / Task / Future
↓
asyncio EventLoop
↓
selectors.DefaultSelector
↓
EpollSelector
↓
Linux epoll
关键结论:
Future 永远不直接接触 epoll
2. selectors 模块的作用
selectors 的职责只有三点:
-
屏蔽平台差异
-
管理 fd → callback 映射
-
提供统一的
select()接口
在 Linux 上:
DefaultSelector = EpollSelector
3. fd 注册过程
loop.add_reader(fd, callback)
真实流程:
asyncio → selector.register(fd, EVENT_READ, callback)
→ epoll.register(fd, EPOLLIN)
→ Python 保存 fd → callback 映射
epoll 只知道 fd,不知道 Future、Task、协程。
4. epoll 事件如何唤醒协程
完整路径:
epoll_wait 返回 fd
↓
EpollSelector 查 fd_to_key
↓
取出 key.data(callback)
↓
callback 内部调用 Future.set_result()
↓
Future 完成
↓
Task 进入 ready 队列
↓
协程继续执行
核心思想:
epoll 是触发器,Future 是状态,事件循环是翻译器
七、非 I/O 的 Future:统一抽象的关键
await asyncio.sleep(1)
这里:
-
没有 epoll
-
只有定时器
流程:
-
创建 Future
-
放入定时器堆
-
到期时
Future.set_result() -
Task 恢复
这说明:
Future 是通用等待模型,epoll 只是其中一种完成来源
八、取消(cancel)的真实实现
task.cancel()
底层机制:
-
给 Task 注入
CancelledError -
下次 resume 时:
coro.throw(CancelledError)
- 若未捕获 → 协程结束
取消不是“杀线程”,而是异常控制流。
九、为什么事件循环必须是单线程
如果多线程:
-
Task 状态要锁
-
Future 状态要锁
-
ready 队列要锁
asyncio 的取舍非常明确:
用单线程换确定性和低切换成本
并发靠 I/O,而不是 CPU。
十、核心总结(压缩版)
-
协程是函数状态机
-
Task 驱动协程执行
-
Future 管理等待状态
-
await 是显式让出控制权
-
事件循环是用户态调度器
-
selector 做 fd → callback 映射
-
epoll 只是 I/O 就绪信号源
asyncio 的本质不是“快”,而是“高并发且可控”。
结语
理解 asyncio,关键不是记 API,而是建立这条清晰的认知链:
epoll → selector → callback → Future → Task → 协程
一旦这条链条在你脑子里跑通:
-
死锁问题会变直观
-
性能瓶颈更容易定位
-
async 代码不再“玄学”