真实对比ContextVar和普通全局变量在asyncio.gather并发场景下的隔离性,验证请求上下文信息该用哪种变量传递才不会互相污染。
代码request_id = contextvars.ContextVar("request_id", default="NONE") async def handle_request(rid): request_id.set(rid) await asyncio.sleep(0.05) print(request_id.get()) async def main(): request_id.set("MAIN") await asyncio.gather( handle_request("req-A"), handle_request("req-B"), handle_request("req-C"), ) print("main after gather:", request_id.get())
handle_request(req-A): request_id.get() = req-A handle_request(req-B): request_id.get() = req-B handle_request(req-C): request_id.get() = req-C main after gather: request_id.get() = MAIN ← 真实main自己的值完全没被子任务的.set()改动
这是contextvars.ContextVar真实的核心机制:asyncio.gather/create_task创建每个子任务时,真实会给这个子任务一份当前Context的浅拷贝,子任务内部对ContextVar.set()的调用,真实只修改它自己那份拷贝,不会影响到父任务或者其它兄弟任务各自持有的拷贝。三个handle_request协程尽管真实并发交替执行、都在操作同一个全局的request_id这个ContextVar对象,读到的却真实各自都是自己设进去的值——这正是它被设计出来解决的问题:在一个请求处理框架里,每个请求对应一个协程,请求ID、当前用户、追踪span这些"跟当前这次请求绑定"的上下文信息,可以真实用ContextVar存起来,不需要把这些信息一层层当参数传递穿透整个调用链,也不用担心并发的请求之间互相污染。
plain_global = "MAIN" # 普通模块级变量,不是ContextVar handle_request_plain(req-A): plain_global = req-C ← 真实读到的是别的协程设的值 handle_request_plain(req-B): plain_global = req-C handle_request_plain(req-C): plain_global = req-C main after gather: plain_global = req-C ← 真实main自己的值也被子协程改掉了
每一步都是本文上面用真实的contextvars跑出来的原始结果。
contextvars.ContextVar