async/await语法与事件循环

Python并发编程专题 · Python异步编程的基石

专题:Python并发编程系统学习

关键词:Python, 并发编程, async, await, 协程, 事件循环, asyncio, coroutine

一、协程的基本概念

协程(Coroutine)是一种特殊的函数,它可以在执行过程中暂停,并在之后从暂停的位置继续执行。与传统函数"调用-返回"的线性执行模型不同,协程采用"协作式调度"机制:协程主动让出(yield/await)控制权,由事件循环或调度器决定下一步运行哪个协程。

核心要点:协程是可暂停和恢复的函数,与普通函数的"全有或全无"执行模式有本质区别。普通函数一旦被调用就会从头执行到尾,而协程可以在中途挂起,将CPU让给其他协程使用。

async def定义协程函数:在Python中,使用 async def 语法声明一个协程函数。调用协程函数不会立即执行函数体,而是返回一个协程对象(coroutine object)。该对象需要通过事件循环驱动才能真正运行。

async def my_coro(): return 42 # 调用协程函数返回协程对象,不会执行函数体 coro = my_coro() # 需要 await 或事件循环来驱动执行 # result = await my_coro() # 在另一个协程中 print(coro) # <coroutine object my_coro at 0x...>

协作式调度:协程的调度是非抢占式的。每个协程必须主动调用 await 才有可能让出执行权。这意味着协程不会被操作系统强行打断(不像线程的抢占式调度),因此协程内的临界区天然具有原子性,无需加锁保护。

二、async/await语法入门

async/await 是Python 3.5引入的协程语法糖,为异步编程提供了清晰、直观的表达方式。async def 用于定义协程函数,await 用于等待可等待对象的完成,同时挂起当前协程。

import asyncio async def hello(): print("Hello") await asyncio.sleep(1) # 协程的挂起点 print("World") asyncio.run(hello())

运行上述代码,输出顺序为:先打印"Hello",然后暂停1秒,再打印"World"。在 await asyncio.sleep(1) 这行,当前协程被挂起,控制权交还给事件循环。这1秒内事件循环可以调度其他协程运行,实现并发效果。

如果没有 await 关键字,协程将按同步方式阻塞执行。正是 await 的存在,使事件循环能在等待I/O时切换到其他任务,从而提升整体效率。

三、事件循环机制

事件循环(Event Loop)是异步编程的核心调度器,负责管理协程的执行顺序和调度策略。它本质上是一个无限循环,不断从任务队列中取出就绪的协程并执行,同时监听I/O事件并在事件发生时唤醒对应的回调或协程。

事件循环的调度模型:

关键理解:事件循环是"单线程+协作式多任务"的实现。与多线程不同,协程的切换点是确定的(await处),不存在竞态条件,因此不需要锁。但这也意味着如果一个协程内部执行了耗时很长的CPU计算而没有await,它将会阻塞整个事件循环,导致所有其他协程无法运行。

四、await表达式的本质

await 表达式的本质是一个挂起点(suspension point)。当协程执行到 await 时,会发生以下过程:

  1. 检查可等待对象:Python解释器检查 await 后面的对象是否实现了 __await__ 方法(即是否是一个可等待对象)。
  2. 挂起当前协程:如果可等待对象还未完成,当前协程的执行被暂停,其状态(栈帧、局部变量等)被保存。
  3. 控制权交还事件循环:协程将控制权交还给事件循环,事件循环从任务队列中选择下一个就绪的协程继续执行。
  4. 恢复执行:当可等待对象完成时(如I/O操作结束、定时器到期),事件循环将当前协程重新放入就绪队列,在合适的时机恢复执行。
async def fetch_data(): # await 挂起当前协程,等待网络请求完成 data = await some_io_operation() # 恢复执行:数据已准备好 return data

五、可等待对象(Awaitable)

在Python异步编程中,可等待对象(Awaitable)是指实现了 __await__ 方法的对象,可以在 await 表达式中使用。有三种主要的可等待对象类型:

类型说明典型用法
coroutine通过 async def 定义的协程函数返回的对象await my_coro()
Task对协程的封装,提供任务级别的管理和调度await asyncio.create_task(coro())
Future异步操作结果的占位符,在将来某个时间点会持有结果await loop.create_future()

三者的区别与联系:

六、asyncio.run()详解

asyncio.run() 是Python 3.7引入的高阶API,是运行异步程序的推荐入口函数。它封装了事件循环的完整生命周期管理:

import asyncio async def main(): await asyncio.sleep(1) return "完成" # 一站式入口:创建事件循环 → 运行协程 → 关闭事件循环 result = asyncio.run(main()) print(result) # 完成

asyncio.run() 的幕后操作:

  1. 创建新的事件循环实例。
  2. 将传入的协程包装为一个 Task,加入事件循环。
  3. 运行事件循环直到协程完成(类似于 run_until_complete)。
  4. 协程完成后,关闭事件循环并清理资源。
  5. 返回协程的返回值。

参数 debug:asyncio.run(coro, debug=True) 可以启用调试模式,在调试模式下,事件循环会记录更多的调度信息(如协程的创建位置、await耗时等),有助于发现潜在的阻塞和性能问题。

最佳实践:在每个异步程序的入口处,只调用一次 asyncio.run()。不要在已运行的事件循环中重复调用它,这会引发 RuntimeError。应使用 asyncio.create_task() 来并发运行多个协程。

七、低阶API:get_event_loop/run_until_complete

asyncio.run() 出现之前(Python 3.4-3.6),开发者需要手动管理事件循环的生命周期:

import asyncio async def main(): await asyncio.sleep(1) return "旧风格API" # Python 3.7之前的旧方式(不推荐) loop = asyncio.get_event_loop() result = loop.run_until_complete(main()) loop.close() print(result)

新老API对比:

特性高阶API (asyncio.run)低阶API (get_event_loop)
引入版本Python 3.7Python 3.4
事件循环管理自动创建和关闭手动管理
安全性防止重复使用已关闭的循环可能误用已关闭的循环
调试模式通过 debug 参数通过 set_debug()

已弃用警告:从Python 3.10开始,asyncio.get_event_loop() 在非运行事件循环的线程中调用时会发出 DeprecationWarning。Python官方强烈推荐使用 asyncio.run() 作为统一入口。

八、协程执行流程示例

以下示例展示了多个协程通过事件循环协作调度的完整执行流程:

async def step1(): print("step1开始") await asyncio.sleep(1) # 挂起1秒 print("step1恢复") return "结果1"
async def step2(): print("step2开始") await asyncio.sleep(2) # 挂起2秒 print("step2恢复") return "结果2" async def main(): # 同时调度两个协程 t1 = asyncio.create_task(step1()) t2 = asyncio.create_task(step2()) # 等待两个任务完成 r1, r2 = await asyncio.gather(t1, t2) print(f"结果: {r1}, {r2}") asyncio.run(main())

执行流程分析:

  1. asyncio.run(main()) 创建事件循环并运行 main 协程。
  2. main() 中通过 create_taskstep1step2 封装为Task,加入事件循环的任务队列,立即开始执行。
  3. step1 打印"step1开始",遇到 await asyncio.sleep(1) 挂起。
  4. step2 打印"step2开始",遇到 await asyncio.sleep(2) 挂起。
  5. 事件循环空闲,等待定时器到期。1秒后 step1 恢复,打印"step1恢复"并完成。
  6. 再等待1秒(总计2秒),step2 恢复,打印"step2恢复"并完成。
  7. asyncio.gather 收集两个结果,main 协程打印最终结果。
  8. 全部协程完成后,事件循环自动关闭。

整个过程中,step1 和 step2 是并发执行的,总耗时约为2秒(取最慢的任务),而非4秒(串行执行)。这正是异步编程通过事件循环调度带来的并发优势。