Python协程的调度并不依赖操作系统线程切换,而是由语言运行时的事件循环在单一线程内完成协作式多任务管理。每个协程在执行到await表达式时主动挂起,事件循环借此机会运行其他就绪任务,从而实现高并发的IO处理能力。

协程与事件循环的基础关系
在asyncio框架中,协程本质上是一个可以暂停和恢复的函数对象。当我们使用async def定义函数时,调用它并不会立即执行函数体,而是返回一个协程对象。这个对象必须被事件循环调度才能运行。事件循环是整个异步系统的核心枢纽,它负责维护就绪队列、定时器和IO多路复用器(如epoll、kqueue)。
事件循环在启动后进入无限循环,每一步都会检查定时器是否到期、IO事件是否就绪,并从就绪队列中取出协程继续执行。当协程内部遇到await时,控制权通过生成器式的yield交还给循环,循环再调度下一个任务。这种机制让成千上万个协程可以共用一个线程,避免了线程上下文切换的开销。
await背后的挂起与恢复原理
从底层看,Python的协程是基于生成器改进的协程(coroutine),await相当于一种特殊的yield from。当执行到await some_awaitable时,解释器会暂停当前协程,并等待被await对象返回一个结果。这个被await的对象通常是一个Future或另一个协程,它们由事件循环在合适时机标记为完成。
下面的代码展示了协程如何被事件循环逐步调度。我们定义了两个简单协程,并通过asyncio.run启动它们:
import asyncio
async def task_a():
print("A开始")
await asyncio.sleep(1)
print("A结束")
async def task_b():
print("B开始")
await asyncio.sleep(0.5)
print("B结束")
async def main():
# 并发创建两个任务,由事件循环调度
await asyncio.gather(task_a(), task_b())
asyncio.run(main())
运行后你会看到A开始、B开始先打印,随后B结束、A结束。这说明task_a在await asyncio.sleep(1)时挂起,循环立刻去执行task_b,而不是干等。asyncio.sleep本身不会阻塞线程,只是向循环注册了一个定时器,时间到了再把协程放回就绪队列。
事件循环的调度数据结构
asyncio的事件循环内部主要使用两种结构管理任务:一个是用于定时回调的堆(最小堆),另一个是用于存放立即就绪协程的双向队列。当协程await一个Future时,如果Future尚未完成,协程会被挂起并作为回调绑定到Future上;一旦Future被set_result,循环就把协程重新入队。
下面的表格对比了同步阻塞与异步调度在资源使用上的差异:
| 方式 | 并发模型 | 切换成本 | 适用场景 |
|---|---|---|---|
| 多线程 | 抢占式 | 高(内核态切换) | CPU密集、阻塞型旧代码 |
| 协程 | 协作式 | 低(用户态切换) | 高并发IO、网络服务 |
正因为协程切换只发生在await点,所以如果在一个协程里写了time.sleep(3)这种同步阻塞调用,整个事件循环会被卡住,其他协程全部无法推进。这是新手最容易踩的坑。
实战中如何避免调度陷阱
要保证协程被正确调度,所有可能耗时的操作都必须使用异步版本。例如文件读写应使用aiofiles,网络请求使用aiohttp,而不是requests。如果必须调用阻塞函数,可以用loop.run_in_executor丢到线程池,让循环在后台线程等待时不阻碍其他协程。
以下示例演示了错误与正确的写法对比:
import asyncio
import time
# 错误:阻塞整个循环
async def bad_task():
time.sleep(2) # 千万不要这样写
print("坏任务完成")
# 正确:交给线程池执行
async def good_task():
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, time.sleep, 2)
print("好任务完成")
async def main():
await asyncio.gather(bad_task(), good_task())
asyncio.run(main())
在bad_task中,time.sleep会让单线程的事件循环停止响应两秒;而good_task通过executor把阻塞调用移出主线程,协程本身只是await一个Future,调度器依然可以自由运行其他任务。掌握这种分工,才能写出真正高效的异步程序。
总结协程调度的核心要点
Python协程调度是事件循环驱动的协作式多任务:协程在await处让出控制权,循环依据就绪队列和定时器决定下一个执行对象。理解这一机制,就能明白为何异步代码里不能混用阻塞调用,也知道如何通过asyncio提供的原语精确控制任务并发与顺序。
当你后续阅读asyncio源码或排查并发 Bug 时,只需记住一条主线:所有协程都生存在循环之中,唯有主动await,才能彼此成全。