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 运行时中的协作关系:
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 异步编程核心流程总结
- 定义协程: 使用
async def声明异步函数,内部用await挂起 I/O 操作。 - 创建 Task: 通过
asyncio.create_task()将协程包装为 Task,自动加入事件循环调度队列。 - 并发等待: 使用
asyncio.gather()、asyncio.wait()或asyncio.as_completed()管理多个 Task。 - 运行入口: 通过
asyncio.run(main())启动事件循环,执行主协程。