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 基于 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><span>立即学习</span>“<a href="https://www.tulingxueyuan.cn/d/file/p/20260730/1ryximk4b2v style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">Python免费学习笔记(深入)</a>”;</p><p>async def request_id_middleware(request: Request, call_next):</p><p class="aritcle_card flexRow">
<p class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/gongju/2506" title="Python 3.14.2"><img
src="https://img.php.cn/upload/manual/001/21/864/6a696bf37.png" alt="Python 3.14.2" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<p class="aritcle_card_info flexColumn">
<a href="/xiazai/gongju/2506" title="Python 3.14.2">Python 3.14.2</a>
<p>Python 3.14.2是Python编程语言于2025年12月5日发布的稳定版,第二次维护更新属于3.14系列。该版本包括18个修复项目,重点解决了多过程、数据类和正则表达式模块的回归问题,并修复了CVE-2025-12084等安全漏洞。该版本包括18个修复项目,重点解决了多过程、数据和正则表达模块的回归问题,并修复了CVE-2025-12084等安全漏洞。这个版本标志着Python发展的重要里程碑,自由线程模式(去除GIL)正式得到官方支持。</p>
</p>
<a href="/xiazai/gongju/2506" title="Python 3.14.2" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</p>
</p><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 默认不传播 contextvars,尤其当你用 session.request() 发起子协程请求时,子协程 context 是干净的。目前需要显式。 context 拷贝过去。
import contextvars import aiohttp <p>async def fetch_with_context(url): current_ctx = contextvars.copy_context() async with aiohttp.ClientSession() as session:</p><h1>手动将当前 context 注入子协程执行</h1><pre class="brush:php;toolbar:false;"> 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 调用
一旦进入 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 慢慢查询数据库后断开。