专题:Python并发编程系统学习
关键词:Python, 并发编程, async, await, 协程, 事件循环, asyncio, coroutine
协程(Coroutine)是一种特殊的函数,它可以在执行过程中暂停,并在之后从暂停的位置继续执行。与传统函数"调用-返回"的线性执行模型不同,协程采用"协作式调度"机制:协程主动让出(yield/await)控制权,由事件循环或调度器决定下一步运行哪个协程。
核心要点:协程是可暂停和恢复的函数,与普通函数的"全有或全无"执行模式有本质区别。普通函数一旦被调用就会从头执行到尾,而协程可以在中途挂起,将CPU让给其他协程使用。
async def定义协程函数:在Python中,使用 async def 语法声明一个协程函数。调用协程函数不会立即执行函数体,而是返回一个协程对象(coroutine object)。该对象需要通过事件循环驱动才能真正运行。
协作式调度:协程的调度是非抢占式的。每个协程必须主动调用 await 才有可能让出执行权。这意味着协程不会被操作系统强行打断(不像线程的抢占式调度),因此协程内的临界区天然具有原子性,无需加锁保护。
async/await 是Python 3.5引入的协程语法糖,为异步编程提供了清晰、直观的表达方式。async def 用于定义协程函数,await 用于等待可等待对象的完成,同时挂起当前协程。
运行上述代码,输出顺序为:先打印"Hello",然后暂停1秒,再打印"World"。在 await asyncio.sleep(1) 这行,当前协程被挂起,控制权交还给事件循环。这1秒内事件循环可以调度其他协程运行,实现并发效果。
如果没有 await 关键字,协程将按同步方式阻塞执行。正是 await 的存在,使事件循环能在等待I/O时切换到其他任务,从而提升整体效率。
事件循环(Event Loop)是异步编程的核心调度器,负责管理协程的执行顺序和调度策略。它本质上是一个无限循环,不断从任务队列中取出就绪的协程并执行,同时监听I/O事件并在事件发生时唤醒对应的回调或协程。
事件循环的调度模型:
await 操作时,事件循环会为此注册一个回调,等待条件满足时自动唤醒该协程。关键理解:事件循环是"单线程+协作式多任务"的实现。与多线程不同,协程的切换点是确定的(await处),不存在竞态条件,因此不需要锁。但这也意味着如果一个协程内部执行了耗时很长的CPU计算而没有await,它将会阻塞整个事件循环,导致所有其他协程无法运行。
await 表达式的本质是一个挂起点(suspension point)。当协程执行到 await 时,会发生以下过程:
await 后面的对象是否实现了 __await__ 方法(即是否是一个可等待对象)。在Python异步编程中,可等待对象(Awaitable)是指实现了 __await__ 方法的对象,可以在 await 表达式中使用。有三种主要的可等待对象类型:
| 类型 | 说明 | 典型用法 |
|---|---|---|
| coroutine | 通过 async def 定义的协程函数返回的对象 | await my_coro() |
| Task | 对协程的封装,提供任务级别的管理和调度 | await asyncio.create_task(coro()) |
| Future | 异步操作结果的占位符,在将来某个时间点会持有结果 | await loop.create_future() |
三者的区别与联系:
async def 函数产生。await一个协程会直接执行它直到完成。asyncio.create_task() 可以将协程包装为Task并安排其立即执行。asyncio.run() 是Python 3.7引入的高阶API,是运行异步程序的推荐入口函数。它封装了事件循环的完整生命周期管理:
asyncio.run() 的幕后操作:
参数 debug:asyncio.run(coro, debug=True) 可以启用调试模式,在调试模式下,事件循环会记录更多的调度信息(如协程的创建位置、await耗时等),有助于发现潜在的阻塞和性能问题。
最佳实践:在每个异步程序的入口处,只调用一次 asyncio.run()。不要在已运行的事件循环中重复调用它,这会引发 RuntimeError。应使用 asyncio.create_task() 来并发运行多个协程。
在 asyncio.run() 出现之前(Python 3.4-3.6),开发者需要手动管理事件循环的生命周期:
新老API对比:
| 特性 | 高阶API (asyncio.run) | 低阶API (get_event_loop) |
|---|---|---|
| 引入版本 | Python 3.7 | Python 3.4 |
| 事件循环管理 | 自动创建和关闭 | 手动管理 |
| 安全性 | 防止重复使用已关闭的循环 | 可能误用已关闭的循环 |
| 调试模式 | 通过 debug 参数 | 通过 set_debug() |
已弃用警告:从Python 3.10开始,asyncio.get_event_loop() 在非运行事件循环的线程中调用时会发出 DeprecationWarning。Python官方强烈推荐使用 asyncio.run() 作为统一入口。
以下示例展示了多个协程通过事件循环协作调度的完整执行流程:
执行流程分析:
asyncio.run(main()) 创建事件循环并运行 main 协程。main() 中通过 create_task 将 step1 和 step2 封装为Task,加入事件循环的任务队列,立即开始执行。step1 打印"step1开始",遇到 await asyncio.sleep(1) 挂起。step2 打印"step2开始",遇到 await asyncio.sleep(2) 挂起。asyncio.gather 收集两个结果,main 协程打印最终结果。整个过程中,step1 和 step2 是并发执行的,总耗时约为2秒(取最慢的任务),而非4秒(串行执行)。这正是异步编程通过事件循环调度带来的并发优势。