ContextVars 比 thread-local 更适合 asyncio,因为协程切换时不丢失状态,而 threading.local() 在 await 后失效;ContextVar 绑定到协程上下文,支持请求级变量(如 trace_id)自动透传,但需注意 reset、跨 executor 手动传递及同步代码中失效等问题。

ContextVars 为什么比 thread-local 更适合 asyncio
因为 asyncio 的协程会在单线程内频繁切换,threading.local() 在协程间不保留状态——你刚 set 进去的值,await 一下就丢了。而 contextvars.ContextVar 是绑定到每个协程的执行上下文(contextvars.Context)上的,只要没显式重置或跨 context 调用,变量值就能稳定传递。
典型场景:在 FastAPI 或 Quart 中为每个请求注入 trace_id、user_id、request_id 等标识,后续所有 async 函数调用(包括数据库操作、HTTP 客户端请求、日志记录)都能直接读取,不用层层传参。
-
ContextVar实例必须全局定义一次,不能在函数里重复创建(否则每次都是新变量) - 不要用
set()直接改写已有 context —— 应该用ctx.run()或依赖框架自动管理上下文(如 Starlette 的 middleware) - 异步生成器、子任务(
asyncio.create_task())默认继承父 context,但loop.run_in_executor()会丢失,需手动拷贝
如何在 FastAPI 请求生命周期中注入并读取 ContextVar
FastAPI 基于 Starlette,其中间件天然支持 contextvars。最稳妥的方式是在 middleware 中创建并设置 ContextVar,然后在任意后续 async 函数中用 .get() 读取。
from contextvars import ContextVar
from fastapi import FastAPI, Request, Response
import asyncio
<p>request_id_var = ContextVar('request_id', default=None)</p><p>async def request_id_middleware(request: Request, call_next):</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/gongju/2789" title="FastAPI 0.140.10"><img
src="https://img.php.cn/upload/manual/001/221/864/6aabb8c8768f3818.png" alt="FastAPI 0.140.10" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/gongju/2789" title="FastAPI 0.140.10" class="overflowclass">FastAPI 0.140.10</a>
<p class="overflowclass">FastAPI 0.140.10 是 FastAPI 的官方历史稳定版本,下载地址使用 PyPI wheel 包直链,适合指定版本安装和项目环境复现。</p>
</div>
<a rel="nofollow" href="/xiazai/gongju/2789" title="FastAPI 0.140.10" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><h1>每次请求生成唯一 ID 并绑定到当前 context</h1><pre class="brush:php;toolbar:false;">request_id = str(asyncio.current_task().get_coro()).split(' ')[-1].strip('>')
token = request_id_var.set(request_id)
try:
return await call_next(request)
finally:
request_id_var.reset(token) # 必须 reset,避免 context 泄漏app = FastAPI() app.middleware('http')(request_id_middleware)
@app.get('/') async def home(): rid = request_id_var.get() # 这里能拿到值 return {'request_id': rid}
- 别用
uuid4()生成 request_id —— 如果你在中间件里 await 了别的协程,可能触发 context 切换导致get()返回 default - reset() 必须放在
finally块里,否则异常时变量残留会影响下一个请求 - 如果用
BackgroundTasks,它内部是独立 task,仍能继承 context;但若用了run_in_executor,就得手动传入contextvars.copy_context()
在 aiohttp 客户端请求中保持 context 传递
aiohttp 默认不传播 contextvars,尤其当你用 session.request() 发起子请求时,子协程的 context 是干净的。需要显式把当前 context 拷贝过去。
return await current_ctx.run(session.get, url)
- 直接调用
session.get(url)不会继承 context —— 因为底层asyncio.create_task()创建的是新 context -
contextvars.copy_context()获取的是当前协程的完整 context 快照,.run()保证子调用运行在此快照下 - 如果你封装了通用 HTTP client 类,建议在
__aenter__里保存 context,在每次请求方法里用.run()包裹实际 IO 调用
常见错误:在同步代码里误用 ContextVar.get()
一旦进入 run_in_executor 或 C 扩展(如 psycopg2 同步驱动),当前协程 context 就失效了。request_id_var.get() 会返回 default 值(比如 None),而不是你期望的请求 ID。
- 不要在
loop.run_in_executor()回调函数里直接调用.get()—— 应该在调用前用.get()提前取出值,作为参数传进去 - SQLAlchemy 1.4+ 的 async engine 支持 contextvars,但老版本或纯 psycopg2 需要手动透传
- 日志库(如 structlog)若配置了 contextvars 绑定,也要确认它是否在 executor 内正确捕获了变量值
contextvars 不是魔法,它只在 asyncio 协程链路里可靠;跨执行模型时,得靠显式传值兜底。这点容易被忽略,直到 trace 断在某个数据库慢查询之后。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










