应使用 contextvars.ContextVar 而非 global 变量加 asyncio.Lock:因后者无法解决“读-改-写”非原子问题,而 ContextVar 自动隔离协程状态,避免数据污染。

直接用 global 变量 + asyncio.Lock 无法保证安全,除非你只做单次赋值;绝大多数“需要全局状态”的场景,其实该用 contextvars.ContextVar。
asyncio.Lock 为什么不能防住 counter += 1
因为 counter += 1 不是原子操作:它先读 counter,再算 counter + 1,最后写回。即使加了 async with lock:,两个协程仍可能先后读到同一个旧值,各自加 1 后都写回去,结果只 +1 而非 +2。
- 典型错误现象:
counter并发自增 10 次,最终值是 7 或 8,而不是 10 - 日志里看到不同请求混用了同一用户 ID 或 trace_id,说明读写中间态被污染
-
asyncio.Lock只阻塞“进入临界区”,不阻塞“读-改-写”三步之间的切换 - 如果必须用全局变量,只允许
counter = new_value这种单次写入,且所有修改都走同一封装函数
contextvars.ContextVar 才是异步上下文的正确解法
HTTP 请求 ID、当前用户、数据库 session、超时配置……这些数据天然属于“每个协程一份”,不是“所有协程共用一份”。contextvars.ContextVar 正是为此设计:它自动随协程流转,隔离彼此状态,无需锁。
- 声明:
request_id = contextvars.ContextVar("request_id", default=None) - 设值:
request_id.set("req-abc123")(在协程内调用) - 取值:
request_id.get()(只拿到本协程设的值,绝不会串) - 不适用于跨协程传递数据,但正因如此,它比锁更轻量、无死锁风险、无调度阻塞
什么时候真该用 asyncio.Queue 而不是全局变量
当多个协程之间要传递数据、且存在生产/消费关系时,asyncio.Queue 是比全局变量 + 锁更自然、更安全的选择。它内置线程/协程安全,还带背压和等待机制。
- 错误用法:
global items; items.append(x)+asyncio.Lock—— 依然要处理读写竞态、边界判断、唤醒逻辑 - 正确用法:
await queue.put(x)和item = await queue.get(),失败自动重试,queue.task_done()+await queue.join()控制完成时机 -
asyncio.Queue和queue.Queue不能混用:后者是同步阻塞的,放进async函数会卡死整个事件循环 - 适合场景:日志收集器、任务分发器、流式响应组装器
最容易被忽略的一点:你以为在共享状态,其实只是没想清楚数据归属。协程不是线程,没有共享内存的默认契约;contextvars 不是“高级技巧”,而是异步编程中上下文隔离的基础设施——漏掉它,锁也救不回来。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











