Python asyncio事件循环采用协作式调度,通过就绪队列管理任务,协程仅在await时挂起并由I/O就绪或定时器到期唤醒,无抢占机制。

Python asyncio 的事件循环通过维护多个任务队列,并结合“轮询+挂起+唤醒”机制来调度协程任务,核心在于将可执行任务与等待中的任务分离管理,确保 I/O 密集型操作不阻塞整个程序。
任务被注册到事件循环后如何排队
当你调用 asyncio.create_task() 或 loop.create_task(),协程对象会被包装成 Task 实例,并立即加入事件循环的“就绪队列”(ready queue)。这个队列是一个双向队列(deque),新任务通常追加到末尾,调度时从头部取出执行。
- 刚创建或刚被唤醒的任务,进入就绪队列,等待下一轮循环执行
- 任务在 await 某个 awaitable(如 asyncio.sleep()、StreamReader.read())时,会主动让出控制权,并可能注册回调或等待特定事件(如文件描述符可读)
- 事件循环本身不“抢占”任务,而是依赖协程自愿挂起(await)和事件触发后的回调唤醒
事件循环主循环如何推进任务
事件循环的核心是 run_once()(内部方法)或用户可见的 run_forever(),它每轮循环做三件事:检查就绪队列、处理 I/O 事件、执行定时回调。
- 先清空当前就绪队列:逐个取出任务并 send(None) 恢复其协程——这等价于继续执行到下一个 await
- 接着调用底层事件驱动系统(如 select、epoll、kqueue 或 Windows 的 IOCP)等待 I/O 就绪,但只等待“最紧急的定时器超时时间”或“有 I/O 可读写”
- 一旦 I/O 就绪或定时器到期,对应回调被触发,把相关任务重新推回就绪队列(例如 socket 收到数据后,阻塞在 read() 的协程被唤醒)
await 表达式如何影响调度时机
await 是调度发生的显式边界。一个协程只有在 await 时才可能被暂停,也只在被唤醒后从 await 后续语句继续执行。
立即学习“Python免费学习笔记(深入)”;
- 如果 await 的对象是 asyncio.sleep(0),它立刻让出控制权,当前任务被移出运行中状态,下一轮循环再调度
- 如果 await 的是 asyncio.Lock.acquire() 且锁被占用,任务会被挂起并加入锁的等待队列,直到锁释放时被回调唤醒并重回就绪队列
- 没有 await 的纯同步计算(比如大循环、密集数学运算)会阻塞整个事件循环——这不是调度问题,而是误用
不同优先级/延迟任务怎么处理
asyncio 本身不提供优先级调度,但支持按时间排序的延迟任务:
- call_later(delay, callback) 和 call_at(when, callback) 会把回调插入最小堆(heapq)维护的定时器队列
- 每次循环前,事件循环检查该堆顶是否已到期;若到期,就把对应回调包装为 Task 推入就绪队列
- 高频率的 create_task() + 短 sleep() 并不能实现“高优先级”,只是更快抢占就绪队列头部位置;真需要优先级需自行封装调度逻辑(如用优先队列替换就绪 deque)
不复杂但容易忽略:调度不是由事件循环“决定谁该运行”,而是由协程自己通过 await 主动交出控制权,再由事件就绪或时间到达被动唤回——整个过程是协作式,不是抢占式。










