Python asyncio 事件循环是如何调度任务的?

11次阅读

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

Python asyncio 事件循环是如何调度任务的?

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
  • 接着调用底层事件驱动系统(如 selectepollkqueuewindows 的 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 主动交出控制权,再由事件就绪或时间到达被动唤回——整个过程是协作式,不是抢占式。

text=ZqhQzanResources