Python ContextVar:任务创建后,父任务改值为什么不会传过去
日志中的请求编号从哪里来
一个异步服务同时处理几份文档,底层日志函数想知道当前属于哪次请求。逐层传入编号很清楚,但调用链很深时,也可以用 ContextVar 保存随执行上下文传播的少量信息。它的关键问题是:在哪个时点取得初值,随后一次赋值会影响谁。把它当成普通模块全局变量,会对并发结果产生错误预期。
下面的实验只用标准库,在 Python 3.12.14 上验证。把代码存为 demo.py 后运行 python demo.py 即可,不需要服务器、线程或网络。变量放在模块顶层,默认值为 unset;父任务先设为 parent,创建两个子任务,再改成 later。子任务故意先等待事件,让父任务的第二次赋值确定发生在读取之前。
在任务创建时留住起点
未显式传入 context 时,asyncio.create_task 会复制当前上下文。复制发生在创建任务时,所以晚一点真正开始执行,也不会自动改用父任务的新绑定。事件在这里仅用于控制实验先后;它并不负责复制变量。两个子任务从同一个起点出发,再分别把变量改为 A 和 B。
每次 set 都返回一个恢复用的 token。示例在 finally 中 reset,恢复的是本次赋值之前的状态,而不是简单写回默认值。这样辅助函数嵌套设置同一个变量时,外层原本的编号仍能保住。本文采用显式恢复写法,可以在本次验证所用版本直接运行。
AI概念示意图:父任务创建两个分支,各分支保留自己的上下文绑定。图片用于解释概念,不是运行截图。
import asyncio
from contextvars import ContextVar
request_id = ContextVar("request_id", default="unset")
async def worker(label, gate):
await gate.wait()
inherited = request_id.get()
token = request_id.set(label)
try:
await asyncio.sleep(0)
local = request_id.get()
finally:
request_id.reset(token)
return inherited, local, request_id.get()
async def main():
outer = request_id.set("parent")
try:
gate = asyncio.Event()
tasks = [asyncio.create_task(worker(x, gate))
for x in ("A", "B")]
request_id.set("later")
gate.set()
rows = await asyncio.gather(*tasks)
assert rows == [("parent", "A", "parent"),
("parent", "B", "parent")]
print(rows)
print("parent:", request_id.get())
assert request_id.get() == "later"
finally:
request_id.reset(outer)
print("restored:", request_id.get())
assert request_id.get() == "unset"
asyncio.run(main())沿着输出核对传播方向
第一行得到两个三元组,依次是子任务继承的 parent、任务内自己的 A 或 B,以及恢复后的 parent。第二行是 parent: later,说明父任务的后续赋值没有被子任务改回去;最后一行是 restored: unset,说明父任务外层设置也已经恢复。这里比较的是实际结果,没有依赖两个子任务谁先打印。
排查串号时,可以在创建任务前、任务入口和日志写入处分别读取编号。若入口就是旧编号,先检查创建时点;若中途变了,寻找同一任务中的 set;若退出辅助函数后仍残留,检查异常路径是否执行 reset。token 只能用于匹配的变量和上下文,不能拿去让另一个任务替自己恢复。
绑定隔离不等于对象复制
上下文复制是浅复制。若绑定的是列表,两个任务仍可能拿到同一个列表对象,直接 append 会修改共享内容;换一个新列表再 set 才是改变本任务的绑定。请求编号这类不可变字符串更适合作为入门用法,也更容易检查。
直接 await 一个协程不会凭空创建新的任务边界,协程可能就在当前上下文里修改变量。设计接口时应明确哪些函数负责设置和恢复,哪些只负责读取。ContextVar 也不会替代权限验证;它提供执行期间的上下文信息,信息是否可信仍由写入位置和业务校验决定。把标识的设置集中在请求入口,也有助于审核整条传播路径。


