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 表达式时,会发生以下动作:

  1. 挂起当前协程:保存当前执行上下文(局部变量、指令指针等)。
  2. 让出控制权:将执行权交还给事件循环。
  3. 注册回调:事件循环在等待的可等待对象完成时,会恢复该协程的执行。
  4. 恢复执行:可等待对象返回结果后,协程从挂起点继续执行,并拿到返回值。
可等待对象(Awaitable) 包括三种类型:协程(coroutine)、任务(Task)和 Future。其中 Task 是 Future 的子类,也是实际开发中最常用的可等待对象。

2.3 事件循环:异步调度的核心引擎

事件循环(Event Loop)是异步编程的调度中心,负责监听、调度和执行所有待处理的任务。可以将其理解为一个永不停止的“任务调度器”,其核心工作流程如下:

  1. 轮询就绪队列:检查哪些协程或回调已经准备好执行。
  2. 执行就绪任务:从就绪队列中取出任务并执行,直到遇到 await 挂起或任务完成。
  3. 处理 I/O 事件:通过系统调用(如 epoll、kqueue)监听文件描述符的 I/O 事件。
  4. 调度定时器:检查是否有到期的定时回调(如 asyncio.sleep)。
  5. 重复循环:不断重复上述步骤,直到所有任务完成或事件循环被停止。
常见陷阱: 在事件循环中执行 CPU 密集型操作(如大量计算、文件压缩)会阻塞事件循环,导致其他协程无法被调度。此时应使用 asyncio.to_thread() 或进程池将阻塞操作移出事件循环。
事件循环调度协程生命周期 事件循环 就绪队列 等待队列 协程 A 协程 B 协程 C 协程 D (I/O) 协程 E (sleep) 事件循环从就绪队列取出协程执行,遇到 await 则挂起并移入等待队列;I/O 完成后协程回到就绪队列

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() 处挂起当前协程并切换到另一个协程,从而实现并发效果。

async/await 并发执行时序图 时间 事件循环 协程1 (step_one) 协程2 (step_two) 调度 执行 sleep 调度 执行 sleep 等待 恢复 恢复 事件循环在协程1 sleep 期间切换到协程2 执行,实现并发;两个协程的 sleep 时间重叠,总耗时约 1 秒而非 1.5 秒 关键点:await 是协程主动让出 CPU 的“调度点”,而非阻塞等待

2.6 常见错误模式与避坑指南

错误模式 1:忘记 await — 调用协程函数但未使用 await,会返回一个协程对象而非执行结果。这通常不会报错,但会导致协程永远不会被执行,造成逻辑静默丢失。
错误模式 2:在同步函数中调用异步代码 — 在普通 def 函数中直接 await 会引发 SyntaxError。必须使用 asyncio.run() 或 asyncio.get_event_loop().run_until_complete() 来桥接同步与异步世界。
错误模式 3:阻塞事件循环 — 在协程中使用 time.sleep()、CPU 密集计算或同步 I/O 库(如 requests)会阻塞整个事件循环,导致所有并发协程停滞。应始终使用 asyncio.sleep() 和异步 I/O 库。

2.7 本章小结

核心概念 要点总结
协程 使用 async def 定义,调用返回协程对象;通过 await 挂起和恢复
await 挂起当前协程,让出控制权给事件循环;只能用于可等待对象
事件循环 异步调度的核心引擎,负责轮询就绪队列、处理 I/O 事件和定时器
任务(Task) 协程的并发封装,支持取消、结果获取和异常捕获
并发 vs 并行 异步是单线程并发(交替执行),不是多线程并行(同时执行)