4. 协程与异步语法
在异步网络编程中,协程(Coroutine)是承载并发的基本执行单元。理解协程,不能只停留在“async 函数就是协程”这一层,而要看清它本质上是一个可被暂停和恢复的状态机,以及事件循环如何驱动这个状态机一步步向前推进。本节围绕协程的定义与状态、async/await 语法、生成器与协程的关系、事件循环调度流程,以及同步与异步代码的互操作展开。
协程是什么:可暂停的状态机
普通函数一旦被调用,就会从头执行到尾,中途无法交出控制权。协程则不同:它可以在执行到某个“等待点”时主动挂起,把控制权交还给调用方(通常是事件循环),等到所等待的条件满足后,再从挂起处继续执行。因此,协程在任意时刻都处于某个明确的状态。
以 Python 的协程模型为例,一个协程对象通常经历以下状态:创建后处于 PENDING(尚未启动);第一次被调度时进入 RUNNING;遇到 await 且等待对象未就绪时转入 SUSPENDED;等待条件满足后被重新唤醒回到 RUNNING;正常返回后进入 FINISHED;若抛出未捕获异常则进入 CLOSED 或异常终止状态。事件循环的核心工作,就是不断在这些状态之间迁移协程。
async/await 语法:声明与等待
async def 用于声明一个协程函数。调用它并不会立即执行函数体,而是返回一个协程对象,需要被事件循环调度才会真正运行。await 则用于在协程内部等待另一个可等待对象(协程、Task 或 Future)完成。当执行到 await 时,如果被等待对象尚未就绪,当前协程会挂起,控制权交还事件循环,让其它协程得以运行。
import asyncio
async def fetch(name, delay):
print(f"{name} 开始")
await asyncio.sleep(delay) # 挂起点:交出控制权
print(f"{name} 完成")
return name
async def main():
# 并发调度两个协程
results = await asyncio.gather(
fetch("A", 1),
fetch("B", 2),
)
print("结果:", results)
asyncio.run(main())
在这段示例中,fetch("A", 1) 与 fetch("B", 2) 被 gather 包装成两个 Task 并发执行。当 A 在 sleep(1) 处挂起时,事件循环立刻切换到 B;B 在 sleep(2) 处挂起后,循环等待定时器到期。约 1 秒后 A 被唤醒并完成,约 2 秒后 B 完成,总耗时接近 2 秒而非 3 秒。这正是协程并发的直观体现。
生成器与协程的关系
协程的“可暂停”能力并非凭空而来,它继承自生成器。生成器通过 yield 暂停并产出值,通过 send() 恢复执行并传入值。协程在底层复用了这套暂停/恢复机制,只是语义上更强调“等待”而非“产出”。在 Python 中,async def 定义的协程与生成器共享类似的帧结构,await 在实现上可以理解为一种特殊的 yield:它把控制权交出去,并约定恢复时传入结果。
理解这层关系有助于排查问题。例如,若忘记 await 一个协程调用,得到的只是一个协程对象而不会执行,运行时会给出 “coroutine was never awaited” 的警告。这正是因为协程对象需要被驱动,而驱动它的正是事件循环。
事件循环调度协程的流程
事件循环是异步程序的中枢。它维护一个就绪队列和若干 I/O 事件源,循环执行以下动作:从就绪队列取出一个协程并恢复其执行;协程运行到 await 时若未就绪则挂起并注册回调;循环轮询 I/O 与定时器,把就绪的回调重新放入队列。如此往复,直到所有任务完成。
需要强调的是,事件循环是单线程的。协程的并发是“协作式”的:只有当前协程主动挂起,其它协程才有机会运行。因此,若在协程中执行了长时间不挂起的同步阻塞操作(如 time.sleep 或密集计算),整个事件循环都会被卡住,其它协程无法推进。
同步与异步代码的互操作
实际工程中,异步代码往往需要调用同步库,同步代码也可能需要触发异步任务。互操作的关键是“不要阻塞事件循环”。
- 异步中调用同步阻塞函数:使用
asyncio.to_thread()或loop.run_in_executor()把阻塞调用放到线程池执行,避免卡住事件循环。 - 同步中运行异步代码:使用
asyncio.run()启动一个新的事件循环,适合脚本入口;若已在事件循环内,则不能再次调用run(),应改用await或创建 Task。 - 异步中调用同步回调:通过
loop.call_soon()或loop.call_later()把同步函数排入事件循环,由循环在合适时机调用。
import asyncio, time
def blocking_io():
time.sleep(1) # 同步阻塞
return "done"
async def main():
# 放到线程池,避免阻塞事件循环
result = await asyncio.to_thread(blocking_io)
print(result)
asyncio.run(main())
to_thread 或执行器包装;在同步函数中,不要试图“手动”驱动协程,而应交给 asyncio.run 或已有的事件循环。
time.sleep()。它不会挂起协程,而是阻塞整个线程,导致事件循环停摆。应改用 await asyncio.sleep()。
综上,协程是状态机,async/await 是声明与等待的语法,生成器提供了暂停/恢复的底层能力,事件循环负责调度,而互操作的核心是保护事件循环不被阻塞。把这四者串起来,才能写出真正高效的异步服务。