3. 异步编程基础:Python asyncio 核心概念

在深入 OIDC 授权码流程的异步编排之前,必须牢固掌握 Python asyncio 的底层基石。本章将拆解事件循环、协程、Future 与 Task 四个核心概念,并厘清它们之间的协作关系。这些概念是后续优化 HTTP 调用、管理令牌刷新和编排多阶段授权流程的基础。

3.1 事件循环(Event Loop)—— 异步引擎

事件循环是 asyncio 的运行时核心,负责调度和执行所有异步任务。它本质上是一个无限循环,持续从任务队列中取出就绪的协程并执行,遇到 await 挂起点则切换至下一个任务。

特性 说明
单线程模型 所有协程在同一个线程内交替执行,无 GIL 争抢,但需避免阻塞调用
调度单位 协程(coroutine)与回调(callback)
挂起机制 await 将控制权交还给事件循环,循环选择下一个就绪协程
生命周期 通过 asyncio.run() 创建并运行,结束后关闭所有资源
关键认知: 事件循环不是“并行”,而是“并发”。在 OIDC 授权码流程中,多个 HTTP 请求(如获取授权码、交换令牌、刷新令牌)可以并发等待 I/O,但不会同时占用 CPU。

3.2 协程(Coroutine)—— 异步函数

协程是通过 async def 定义的函数,调用后返回一个协程对象。协程本身不会立即执行,必须被事件循环调度或通过 await 驱动。

async def fetch_token(auth_code: str) -> dict:
    # 模拟 OIDC 令牌端点请求
    await asyncio.sleep(0.1)  # 挂起,让出事件循环
    return {"access_token": "abc", "expires_in": 3600}

# 协程对象,尚未执行
coro = fetch_token("code_123")
# 必须 await 或交给事件循环
result = await coro
  • 挂起点: 每个 await 都是协程主动让出 CPU 的位置,事件循环在此处切换任务。
  • 不可中断: 协程内部不能包含阻塞式 I/O(如 time.sleep()、requests.get()),否则会阻塞整个事件循环。
  • 链式调用: 一个协程可以 await 另一个协程,形成调用链,适合编排 OIDC 的多步流程。

3.3 Future 与 Task —— 异步结果容器

Future 和 Task 都代表一个“将来会完成”的操作结果,但用途和抽象层次不同。

对比维度 Future Task
本质 底层异步结果占位符,可手动设置结果/异常 协程的包装器,自动调度并管理生命周期
创建方式 loop.create_future() asyncio.create_task(coro)
自动调度 否,需手动绑定回调或 await 是,创建后立即被事件循环调度
使用场景 底层库实现、回调适配 日常异步任务编排、并发执行
结果获取 future.result() 或 await future await task
实践建议: 在 OIDC 流程优化中,99% 的场景使用 Task。例如并发发起授权码请求和令牌刷新预检查,使用 asyncio.gather(*tasks) 统一等待。

3.4 核心关系图:事件循环、协程、Future 与 Task

下图展示了四个核心概念在 asyncio 运行时中的协作关系:

asyncio 核心概念关系图 asyncio 核心概念协作关系 事件循环 (Event Loop) 协程 (Coroutine) Future Task await 注册 设置结果 调度执行 包装 继承 💡 协程通过 await 注册到事件循环;Task 是协程 + 自动调度的包装,继承自 Future; Future 是底层结果容器,通常由库内部使用,开发者主要操作 Task。

3.5 实战对比:手动 Future vs Task 自动调度

以下代码展示 Future 和 Task 在 OIDC 令牌刷新场景中的差异:

import asyncio

async def refresh_token_async():
    """模拟异步令牌刷新"""
    await asyncio.sleep(0.2)
    return {"access_token": "new_token", "expires_in": 3600}

# ===== 方式一:手动 Future(不推荐) =====
async def manual_future_example():
    loop = asyncio.get_running_loop()
    future = loop.create_future()
    
    # 需要手动绑定回调来设置结果
    async def set_result():
        result = await refresh_token_async()
        future.set_result(result)
    
    asyncio.create_task(set_result())  # 仍需 Task 驱动
    return await future  # 等待 Future 完成

# ===== 方式二:Task 自动调度(推荐) =====
async def task_example():
    task = asyncio.create_task(refresh_token_async())
    # Task 创建后立即被调度,无需手动管理
    result = await task
    return result

# 实际使用
async def main():
    # 并发执行多个令牌刷新
    tasks = [asyncio.create_task(refresh_token_async()) for _ in range(5)]
    results = await asyncio.gather(*tasks)
    print(f"刷新了 {len(results)} 个令牌")
常见陷阱: 不要直接 await 一个协程而不创建 Task,这会导致串行执行。例如 await coro1(); await coro2() 是顺序执行,而 await asyncio.gather(coro1(), coro2()) 或创建 Task 才能并发。

3.6 异步编程核心流程总结

  1. 定义协程: 使用 async def 声明异步函数,内部用 await 挂起 I/O 操作。
  2. 创建 Task: 通过 asyncio.create_task() 将协程包装为 Task,自动加入事件循环调度队列。
  3. 并发等待: 使用 asyncio.gather()、asyncio.wait() 或 asyncio.as_completed() 管理多个 Task。
  4. 运行入口: 通过 asyncio.run(main()) 启动事件循环,执行主协程。