登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

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。

Python asyncio Runner 事件循环与 contextvars 上下文分离的静态结构图
图1:说明图展示 asyncio.Runner 复用事件循环与 ContextVar 隔离业务状态的两条边界。

二、用 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 管理。

Python copy_context 与 Runner.run context 参数为不同入口绑定独立请求上下文的结构图
图2:结构图展示 copy_context、ContextVar.set 和 Runner.run(context=ctx) 的绑定关系。

三、让同一入口内的子任务继承正确上下文

如果一个入口内部还要创建多个任务,先决定它们是否应该共享请求级变量。需要同一请求标识时,可以在对应 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 决定。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>