1. 什么是 ContextVar? #

一句话记忆:全局变量全程序共享一份;ContextVar 按「上下文」各存一份。

2. 前置知识:先弄清三个概念 #

2.1 什么是「全局变量」? #

# 说明:定义一个模块级全局变量,初始用户名为 guest
current_user = "guest"

# 说明:定义函数,打印当前全局变量里的用户名
def show_user():
    # 说明:直接读取全局变量(无需通过参数传入)
    print(f"当前用户: {current_user}")

# 说明:修改全局变量
current_user = "alice"

# 说明:调用函数,会打印 alice
show_user()

2.2 什么是「线程」和 threading.local? #

# 说明:导入线程相关模块
import threading

# 说明:创建一个线程局部存储对象
local = threading.local()

# 说明:定义在子线程中运行的函数
def worker(name):
    # 说明:给当前线程自己的存储写入名字(不影响其他线程)
    local.name = name
    # 说明:打印当前线程读到的名字
    print(f"线程读到: {local.name}")

# 说明:创建两个线程,分别写入 alice 和 bob
t1 = threading.Thread(target=worker, args=("alice",))
t2 = threading.Thread(target=worker, args=("bob",))

# 说明:启动两个线程
t1.start()
t2.start()

# 说明:等待两个线程都执行结束
t1.join()
t2.join()

2.3 什么是「协程」? #

# 说明:导入 asyncio,用于运行异步协程
import asyncio

# 说明:定义一个简单的异步函数
async def hello(name):
    # 说明:打印问候语
    print(f"你好, {name}")
    # 说明:异步休眠 0.1 秒(让出执行权,不卡住整个程序)
    await asyncio.sleep(0.1)
    # 说明:休眠结束后再打印一句
    print(f"{name} 说再见")

# 说明:定义主协程,并发跑两个子任务
async def main():
    # 说明:同时等待两个协程完成
    await asyncio.gather(hello("小明"), hello("小红"))

# 说明:启动事件循环并运行 main
asyncio.run(main())

3. 为什么需要 ContextVar? #

先用「全局变量串台」说明问题,再用 ContextVar 给出正确写法。

3.1 问题:全局变量在并发里会串台 #

# 说明:导入 asyncio
import asyncio

# 说明:用普通全局变量记录「当前用户」
current_user = "guest"

# 说明:定义处理请求的异步函数
async def handle_request(user):
    # 说明:声明要修改模块级全局变量
    global current_user
    # 说明:把全局变量改成当前请求的用户
    current_user = user
    # 说明:模拟耗时 I/O,期间其他协程也可能改全局变量
    await asyncio.sleep(0.1)
    # 说明:再次读取全局变量(此时可能已被别的协程改掉)
    print(f"期望用户={user}, 实际读到={current_user}")

# 说明:定义主协程,并发处理两个请求
async def main():
    # 说明:同时处理 alice 和 bob 两个请求
    await asyncio.gather(
        handle_request("alice"),
        handle_request("bob"),
    )

# 说明:运行主协程
asyncio.run(main())

可能输出(顺序不固定,但「串台」很常见):

期望用户=alice, 实际读到=bob
期望用户=bob, 实际读到=bob

3.2 解决:用 ContextVar 隔离每个任务的数据 #

# 说明:导入 asyncio
import asyncio
# 说明:导入 contextvars 模块
import contextvars

# 说明:创建一个名为 user 的上下文变量,默认值为 guest
user_var = contextvars.ContextVar("user", default="guest")

# 说明:定义处理请求的异步函数
async def handle_request(user):
    # 说明:在当前任务的上下文中设置用户名
    user_var.set(user)
    # 说明:模拟耗时 I/O
    await asyncio.sleep(0.1)
    # 说明:从当前上下文读取用户名(不会被其他任务覆盖)
    print(f"期望用户={user}, 实际读到={user_var.get()}")

# 说明:定义主协程
async def main():
    # 说明:并发处理两个请求
    await asyncio.gather(
        handle_request("alice"),
        handle_request("bob"),
    )

# 说明:运行主协程
asyncio.run(main())

输出示例:

期望用户=alice, 实际读到=alice
期望用户=bob, 实际读到=bob

4. 创建与读写 ContextVar #

# 说明:导入 contextvars 模块
import contextvars

# 说明:创建上下文变量 request_id,默认值为 unknown
request_id_var = contextvars.ContextVar("request_id", default="unknown")

# 说明:尚未 set 时,get 会返回默认值
print(request_id_var.get())

# 说明:在当前上下文中写入请求 ID
request_id_var.set("req-1001")

# 说明:再次读取,得到刚写入的值
print(request_id_var.get())

运行结果:

unknown
req-1001

5. 跨函数读取:不必层层传参 #

# 说明:导入 contextvars 模块
import contextvars

# 说明:创建用户名字上下文变量,默认匿名
username_var = contextvars.ContextVar("username", default="匿名")

# 说明:最深层的工具函数:直接从上下文取用户名
def write_log(message):
    # 说明:读取当前上下文中的用户名
    user = username_var.get()
    # 说明:打印带用户名的日志
    print(f"[{user}] {message}")

# 说明:中间层业务函数:也不接收用户名参数
def process_order(order_id):
    # 说明:调用日志函数(用户名从上下文来)
    write_log(f"开始处理订单 {order_id}")
    # 说明:模拟业务处理
    write_log(f"订单 {order_id} 处理完成")

# 说明:入口:先 set,再调用深层函数
def main():
    # 说明:把当前用户写入上下文
    username_var.set("小明")
    # 说明:处理订单,中间无需传递用户名
    process_order("A001")

# 说明:运行入口函数
main()

运行结果:

[小明] 开始处理订单 A001
[小明] 订单 A001 处理完成

6. Token 与 reset:改完再恢复 #

# 说明:导入 contextvars 模块
import contextvars

# 说明:创建语言上下文变量,默认中文
lang_var = contextvars.ContextVar("lang", default="zh")

# 说明:定义一个临时切换语言的函数
def with_english():
    # 说明:设置英文,并保存返回的 token,方便之后还原
    token = lang_var.set("en")
    # 说明:打印当前语言
    print(f"函数内语言: {lang_var.get()}")
    # 说明:用 token 恢复到 set 之前的值
    lang_var.reset(token)
    # 说明:打印恢复后的语言
    print(f"恢复后语言: {lang_var.get()}")

# 说明:先把当前语言设为中文
lang_var.set("zh")

# 说明:打印进入函数前的语言
print(f"进入前语言: {lang_var.get()}")

# 说明:调用临时切换函数
with_english()

# 说明:打印离开函数后的语言(仍是 zh)
print(f"离开后语言: {lang_var.get()}")

运行结果:

进入前语言: zh
函数内语言: en
恢复后语言: zh
离开后语言: zh

7. 在 asyncio 里如何隔离? #

这是 ContextVar 最重要的场景:同一线程里多个协程任务,各自有一份值。

7.1 任务创建时会复制上下文 #

# 说明:导入 asyncio
import asyncio
# 说明:导入 contextvars
import contextvars

# 说明:创建用户上下文变量
user_var = contextvars.ContextVar("user", default="guest")

# 说明:定义子任务:打印自己上下文里的用户
async def worker(tag):
    # 说明:读取当前任务上下文中的用户
    print(f"{tag} 开始: {user_var.get()}")
    # 说明:异步休眠,期间主协程可能改自己的上下文
    await asyncio.sleep(0.05)
    # 说明:再次读取,仍应是创建任务时复制来的值
    print(f"{tag} 结束: {user_var.get()}")

# 说明:定义主协程
async def main():
    # 说明:先把主上下文设为 alice
    user_var.set("alice")
    # 说明:创建任务 A(此时复制到的是 alice)
    task_a = asyncio.create_task(worker("任务A"))
    # 说明:创建任务 B(此时复制到的仍是 alice)
    task_b = asyncio.create_task(worker("任务B"))
    # 说明:主协程把自己的值改成 bob(不影响已创建的任务)
    user_var.set("bob")
    # 说明:打印主协程自己的值
    print(f"主协程: {user_var.get()}")
    # 说明:等待两个子任务结束
    await asyncio.gather(task_a, task_b)

# 说明:运行主协程
asyncio.run(main())

运行结果示例:

主协程: bob
任务A 开始: alice
任务B 开始: alice
任务A 结束: alice
任务B 结束: alice

7.2 让每个任务一创建就有自己的值 #

# 说明:导入 asyncio
import asyncio
# 说明:导入 contextvars
import contextvars

# 说明:创建请求 ID 上下文变量
req_var = contextvars.ContextVar("req_id", default="-")

# 说明:业务函数:从上下文读请求 ID 并打印
def log(msg):
    # 说明:读取当前上下文中的请求 ID
    print(f"[{req_var.get()}] {msg}")

# 说明:模拟一次请求处理
async def handle(req_id):
    # 说明:进入本任务后立刻写入自己的请求 ID
    req_var.set(req_id)
    # 说明:记录开始日志
    log("开始处理")
    # 说明:模拟 IO
    await asyncio.sleep(0.05)
    # 说明:记录结束日志
    log("处理结束")

# 说明:主协程并发两个请求
async def main():
    # 说明:并发运行两个带不同请求 ID 的任务
    await asyncio.gather(
        handle("R-1"),
        handle("R-2"),
    )

# 说明:启动
asyncio.run(main())

运行结果示例(两行的交错顺序可能不同):

[R-1] 开始处理
[R-2] 开始处理
[R-1] 处理结束
[R-2] 处理结束

8. copy_context 与 run:手动开一个「隔离沙箱」 #

# 说明:导入 contextvars
import contextvars

# 说明:创建上下文变量,默认 outer
name_var = contextvars.ContextVar("name", default="outer")

# 说明:先在外层上下文写入 outer-value
name_var.set("outer-value")

# 说明:复制当前上下文,得到一份独立副本
ctx = contextvars.copy_context()

# 说明:定义将在副本上下文中运行的函数
def inside():
    # 说明:副本刚开始时,值和复制那一刻一致
    print(f"沙箱内读取: {name_var.get()}")
    # 说明:在沙箱内修改(只影响副本)
    name_var.set("inner-value")
    # 说明:确认沙箱内已变成新值
    print(f"沙箱内修改后: {name_var.get()}")

# 说明:在复制出的上下文中运行 inside
ctx.run(inside)

# 说明:外层上下文仍然是原来的值
print(f"外层读取: {name_var.get()}")

运行结果:

沙箱内读取: outer-value
沙箱内修改后: inner-value
外层读取: outer-value

9. 与 threading.local 怎么选? #

对比项 threading.local ContextVar
隔离单位 线程 上下文(含协程任务)
同线程多协程 会互相覆盖 可各自独立
典型场景 纯多线程、无 asyncio asyncio / FastAPI 等异步程序
建议 不做异步时可用 写异步就优先用它

10. 易踩的坑 #

10.1 可变对象:改内容不等于换值 #

# 说明:导入 contextvars
import contextvars

# 说明:创建用于存放标签列表的上下文变量,默认空元组
tags_var = contextvars.ContextVar("tags", default=())

# 说明:错误示范:把同一个 list 对象 set 进去再原地修改
bad_list = ["a"]
# 说明:写入可变列表
tags_var.set(bad_list)
# 说明:原地修改列表内容(所有引用该列表的地方都会看到变化)
bad_list.append("b")
# 说明:打印,会看到 a 和 b
print("可变对象陷阱:", tags_var.get())

# 说明:推荐写法:每次用新的不可变对象整体替换
tags_var.set(("a",))
# 说明:需要增加元素时,创建新元组再 set
tags_var.set(tags_var.get() + ("b",))
# 说明:打印推荐写法的结果
print("推荐写法:", tags_var.get())

运行结果:

可变对象陷阱: ['a', 'b']
推荐写法: ('a', 'b')

10.2 忘记设默认值 #

# 说明:导入 contextvars
import contextvars

# 说明:创建一个没有默认值的上下文变量
no_default = contextvars.ContextVar("no_default")

# 说明:尝试安全读取:先捕获 LookupError
try:
    # 说明:从未 set 且无 default,这里会报错
    print(no_default.get())
# 说明:捕获查找失败异常
except LookupError:
    # 说明:打印提示信息
    print("尚未设置,且没有默认值")

# 说明:调用 get 时传入临时默认值,避免异常
print(no_default.get("临时默认"))

运行结果:

尚未设置,且没有默认值
临时默认

11. 综合案例:异步请求日志 #

# 说明:导入 asyncio
import asyncio
# 说明:导入 contextvars
import contextvars

# 说明:请求 ID 上下文变量
request_id_var = contextvars.ContextVar("request_id", default="-")
# 说明:用户名上下文变量
username_var = contextvars.ContextVar("username", default="guest")

# 说明:通用日志函数:自动带上请求 ID 和用户名
def log(message):
    # 说明:从上下文读取请求 ID
    rid = request_id_var.get()
    # 说明:从上下文读取用户名
    user = username_var.get()
    # 说明:打印统一格式的日志
    print(f"{rid} | {user} | {message}")

# 说明:模拟查询数据库(深层函数,不接收用户参数)
async def query_db():
    # 说明:记录查询开始
    log("查询数据库...")
    # 说明:模拟数据库耗时
    await asyncio.sleep(0.05)
    # 说明:记录查询结束
    log("查询完成")

# 说明:模拟一次完整请求
async def handle_request(request_id, username):
    # 说明:进入请求后写入本请求的上下文信息
    request_id_var.set(request_id)
    # 说明:写入当前用户名
    username_var.set(username)
    # 说明:记录请求开始
    log("收到请求")
    # 说明:调用深层业务(无需传参)
    await query_db()
    # 说明:记录请求结束
    log("返回响应")

# 说明:主程序:并发两个请求
async def main():
    # 说明:同时处理两个用户的请求
    await asyncio.gather(
        handle_request("REQ-001", "alice"),
        handle_request("REQ-002", "bob"),
    )

# 说明:作为脚本直接运行时启动
if __name__ == "__main__":
    # 说明:运行主协程
    asyncio.run(main())

运行结果示例(同一请求的三行日志中,请求 ID 与用户始终一致):

REQ-001 | alice | 收到请求
REQ-002 | bob | 收到请求
REQ-001 | alice | 查询数据库...
REQ-002 | bob | 查询数据库...
REQ-001 | alice | 查询完成
REQ-002 | bob | 查询完成
REQ-001 | alice | 返回响应
REQ-002 | bob | 返回响应

12. 小结 #