2. 异步编程基础回顾:async/await原理、事件循环、协程与任务
在深入数据库迁移的异步任务测试验证之前,必须牢固掌握 Python 异步编程的核心基石。本章将系统回顾 async/await 的语法原理、事件循环的调度机制、协程与任务的生命周期,为后续章节中复杂的错误处理模式打下坚实的理论基础。
2.1 协程:异步编程的基本单元
协程(Coroutine)是 Python 异步编程中的最小执行单元。与普通函数不同,协程可以在执行过程中暂停(挂起),并在未来某个时刻恢复执行。这种“可暂停-可恢复”的特性,使得协程能够在等待 I/O 操作(如数据库查询、网络请求)时主动让出 CPU,从而避免阻塞整个线程。
await 关键字可以挂起自身,将控制权交还给事件循环。
定义一个协程函数只需在 def 前加上 async 关键字:
async def fetch_user(user_id: int) -> dict:
# 模拟异步数据库查询
await asyncio.sleep(0.1)
return {"id": user_id, "name": f"User_{user_id}"}
调用协程函数不会立即执行函数体,而是返回一个协程对象。该对象需要被事件循环调度才能实际运行。
2.2 await 关键字:挂起与恢复的契约
await 是协程内部用于挂起当前协程、等待另一个可等待对象(Awaitable)完成的关键字。当协程执行到 await 表达式时,会发生以下动作:
- 挂起当前协程:保存当前执行上下文(局部变量、指令指针等)。
- 让出控制权:将执行权交还给事件循环。
- 注册回调:事件循环在等待的可等待对象完成时,会恢复该协程的执行。
- 恢复执行:可等待对象返回结果后,协程从挂起点继续执行,并拿到返回值。
2.3 事件循环:异步调度的核心引擎
事件循环(Event Loop)是异步编程的调度中心,负责监听、调度和执行所有待处理的任务。可以将其理解为一个永不停止的“任务调度器”,其核心工作流程如下:
- 轮询就绪队列:检查哪些协程或回调已经准备好执行。
- 执行就绪任务:从就绪队列中取出任务并执行,直到遇到
await挂起或任务完成。 - 处理 I/O 事件:通过系统调用(如
epoll、kqueue)监听文件描述符的 I/O 事件。 - 调度定时器:检查是否有到期的定时回调(如
asyncio.sleep)。 - 重复循环:不断重复上述步骤,直到所有任务完成或事件循环被停止。
asyncio.to_thread() 或进程池将阻塞操作移出事件循环。
2.4 任务(Task):协程的并发执行器
任务(asyncio.Task)是对协程的一层封装,它将协程提交给事件循环,并允许协程并发执行。通过 asyncio.create_task() 可以将一个协程包装成任务,并立即安排其执行。
| 特性 | 协程(Coroutine) | 任务(Task) |
|---|---|---|
| 创建方式 | async def func() 调用返回 |
asyncio.create_task(coro) |
| 执行时机 | 需要被 await 或提交给事件循环 |
创建后立即被事件循环调度 |
| 并发能力 | 串行执行(除非嵌套 await) | 可与其他任务并发执行 |
| 取消支持 | 不支持直接取消 | 支持 task.cancel() |
| 结果获取 | 通过 await coro 直接获取 |
通过 await task 获取,或使用 task.result() |
任务的一个核心优势是支持取消操作,这在数据库迁移的超时控制和错误恢复中至关重要。当某个迁移步骤超时时,可以通过取消对应任务来中断正在执行的协程,并触发清理逻辑。
2.5 async/await 执行流程示例
以下代码展示了协程、任务与事件循环的完整交互流程:
import asyncio
async def step_one():
print("Step 1: 开始执行")
await asyncio.sleep(1) # 模拟 I/O 等待
print("Step 1: 完成")
return "数据1"
async def step_two():
print("Step 2: 开始执行")
await asyncio.sleep(0.5)
print("Step 2: 完成")
return "数据2"
async def main():
# 创建两个任务并发执行
task1 = asyncio.create_task(step_one())
task2 = asyncio.create_task(step_two())
# 等待两个任务都完成
result1 = await task1
result2 = await task2
print(f"合并结果: {result1}, {result2}")
# 启动事件循环
asyncio.run(main())
执行上述代码时,事件循环会交替调度 step_one 和 step_two,在 await asyncio.sleep() 处挂起当前协程并切换到另一个协程,从而实现并发效果。
2.6 常见错误模式与避坑指南
await,会返回一个协程对象而非执行结果。这通常不会报错,但会导致协程永远不会被执行,造成逻辑静默丢失。
def 函数中直接 await 会引发 SyntaxError。必须使用 asyncio.run() 或 asyncio.get_event_loop().run_until_complete() 来桥接同步与异步世界。
time.sleep()、CPU 密集计算或同步 I/O 库(如 requests)会阻塞整个事件循环,导致所有并发协程停滞。应始终使用 asyncio.sleep() 和异步 I/O 库。
2.7 本章小结
| 核心概念 | 要点总结 |
|---|---|
| 协程 | 使用 async def 定义,调用返回协程对象;通过 await 挂起和恢复 |
| await | 挂起当前协程,让出控制权给事件循环;只能用于可等待对象 |
| 事件循环 | 异步调度的核心引擎,负责轮询就绪队列、处理 I/O 事件和定时器 |
| 任务(Task) | 协程的并发封装,支持取消、结果获取和异常捕获 |
| 并发 vs 并行 | 异步是单线程并发(交替执行),不是多线程并行(同时执行) |