Python asyncio.Runner 怎样为不同任务切换 contextvars 上下文
来源:17golang原创
时间:2026-11-05 00:15:53 476浏览 收藏
如果程序需要在同一个事件循环里连续调用多个异步入口,asyncio.Runner 很合适;但它不会替你为每次 run() 自动创建全新的 contextvars.Context。要让不同任务的请求标识、租户号或追踪变量互不串值,做法是:为每个入口复制一份上下文,写入该入口的变量,再把它传给 runner.run(coro, context=ctx)。
官方文档:https://docs.python.org/3/library/asyncio-runner.html
Runner负责复用事件循环,Context负责隔离上下文状态,二者不是一回事。copy_context()复制当前上下文,Runner.run(context=...)决定本次入口使用哪一份。- 同一份
Context不要被并发重新进入;并发子任务应在各自上下文中创建或显式传递上下文。
一、先区分 Runner 的事件循环与 ContextVar 上下文
Runner 的价值是让多个顶层异步函数共享一个嵌入式事件循环和默认上下文。共享循环可以减少反复初始化,但默认上下文中的变量修改也可能被下一次入口看到。例如第一个入口把 request_id 设成 req-a,第二个入口没有覆盖它,就会读到上一轮留下的值。
这里的边界很容易混淆:关闭 Runner 会收尾异步生成器、关闭默认执行器并关闭事件循环;它不等于每次运行都清空业务上下文。要隔离业务状态,必须在调用入口处显式准备 Context。

二、用 copy_context 为每个入口复制独立上下文
下面的写法把“准备上下文”和“运行协程”放在一起。copy_context() 先复制当前上下文,ContextVar.set() 只修改副本;随后通过 context=ctx 把副本交给 Runner。set() 返回的 token 用于在当前副本内恢复旧值,适合把临时修改包在 try/finally 中。
import asyncio
import contextvars
# 在模块级创建上下文变量,避免闭包长期持有不可见引用
request_id = contextvars.ContextVar("request_id", default="unknown")
async def handle(name: str) -> str:
# 这里读取的是本次 Runner.run 绑定的上下文
await asyncio.sleep(0)
return f"{name}: {request_id.get()}"
def run_with_request(runner: asyncio.Runner, name: str, value: str) -> str:
# 为本次入口复制上下文,避免直接改 Runner 的默认上下文
ctx = contextvars.copy_context()
ctx.run(request_id.set, value)
# context 参数决定这个协程从哪一份 Context 开始运行
return runner.run(handle(name), context=ctx)
with asyncio.Runner() as runner:
# 两次 run 共享事件循环,但读取到的 request_id 各自独立
print(run_with_request(runner, "first", "req-a"))
print(run_with_request(runner, "second", "req-b"))
预期输出是两行不同的请求标识。这个结果只能说明上下文绑定正确,不能说明两个入口拥有不同事件循环;事件循环仍由同一个 Runner 管理。

三、让同一入口内的子任务继承正确上下文
如果一个入口内部还要创建多个任务,先决定它们是否应该共享请求级变量。需要同一请求标识时,可以在对应 ctx 中启动入口,让子任务继承入口上下文;需要两个任务各自拥有值时,则分别复制上下文并在创建任务时传入。
async def worker(label: str) -> str:
# 子任务从创建时绑定的上下文读取变量
await asyncio.sleep(0)
return f"{label}: {request_id.get()}"
async def run_workers() -> list[str]:
# 每个副本只服务一个任务,避免同一 Context 被并发进入
left = contextvars.copy_context()
left.run(request_id.set, "req-left")
right = contextvars.copy_context()
right.run(request_id.set, "req-right")
task_left = asyncio.create_task(worker("left"), context=left)
task_right = asyncio.create_task(worker("right"), context=right)
# gather 只汇总结果;上下文隔离由 create_task 的 context 参数完成
return await asyncio.gather(task_left, task_right)
这里不要把同一个 Context 同时传给两个并发任务。Context 是可进入的上下文栈,重复并发进入会触发运行时错误;复制两份才是清晰的边界。
四、用清理和检查清单收住边界
| 场景 | 建议 | 检查点 |
|---|---|---|
| 多个入口顺序执行 | 每个入口使用 copy_context() 和 context=... | 第二个入口不能读到第一个入口的请求标识 |
| 一个入口并发多个任务 | 按任务复制 Context,创建任务时显式传入 | 不要并发重入同一 Context |
| 程序退出 | 使用 with asyncio.Runner() 管理生命周期 | 让 Runner 负责循环、异步生成器和默认执行器收尾 |
如果变量只是当前协程的临时数据,优先用局部变量;只有需要跨调用链隐式传递、又不想依赖线程局部变量时,才使用 ContextVar。上下文隔离解决的是状态可见范围,不是取消传播、线程安全或数据库事务问题。
相关问题
只调用一次 Runner.run,还需要传 context 吗?
不一定。若没有跨入口复用状态,默认上下文通常足够;一旦入口代表不同请求或租户,显式传递更容易审查。
copy_context 会复制 ContextVar 对象里的可变对象吗?
它复制的是上下文映射,不会深复制变量值。若值本身是列表或字典,仍要避免多个任务共同修改同一个可变对象。
Runner.close 能不能替代上下文清理?
不能。close() 负责 Runner 的异步资源和事件循环生命周期;请求标识是否隔离仍由每次运行绑定的 Context 决定。
-
346 收藏
-
235 收藏
-
387 收藏
-
447 收藏
-
360 收藏
-
415 收藏
-
345 收藏
-
288 收藏
-
148 收藏
-
482 收藏
-
193 收藏
-
350 收藏
-
373 收藏
-
152 收藏
-
文章 · python教程 | 1天前 | 标准库 · xml · Python教程 · Python xml 流式解析 ElementTree xml.etree.ElementTree iterparse339 收藏
-
424 收藏
-
文章 · python教程 | 1天前 | SQLite · 事务管理 · Python教程 · Python 事务 上下文管理器 sqlite3 autocommit isolation_level Connection257 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习