DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Trio 自由线程环境保留旧 ContextVar 对象,给缓存线程空初始上下文

Trio 的缓存线程在自由线程环境继承初始上下文时会保留旧对象,应采用空初始上下文修复并验证任务值传播。

在 CPython 3.14 自由线程构建中,用 Trio 0.33.0 的 to_thread.run_sync() 执行完任务,调用者已替换 ContextVar 的值,旧对象却仍被引用,可以检查缓存工作线程的初始上下文。普通 GIL 构建没有相同表现时,不应先把所有上下文传播都关闭。

Python 3.14 的线程创建支持指定 context。自由线程环境的默认继承行为可能让新线程复制创建者的 Context,而 Trio 的池线程会继续存在;这个线程自身的初始 Context 因此保留了第一次创建它时的对象。每次任务结束后清理任务函数,并不能释放线程初始上下文里的那份引用。

Trio 0.34.0 包含修复:在 Python 3.14 及以后创建池线程时,明确使用空的 Context()。每次 to_thread.run_sync() 对当前任务上下文的传播仍保留,修复的是池线程创建时额外持有的状态。

python -m pip install "trio==0.34.0"

下面同时检查任务能读取当前 ContextVar,以及任务结束后旧对象能被释放。应在原来有问题的自由线程解释器中执行:

import contextvars
import gc
import weakref
import trio

value = contextvars.ContextVar("value")
class Payload:
    pass

async def main():
    payload = Payload()
    ref = weakref.ref(payload)
    value.set(payload)
    assert await trio.to_thread.run_sync(lambda: id(value.get())) == id(payload)
    value.set(None)
    del payload
    gc.collect()
    assert ref() is None
    value.set("next-task")
    assert await trio.to_thread.run_sync(value.get) == "next-task"

trio.run(main)

弱引用断言比单看进程内存下降更适合这个小例子;分配器可能保留内存页,RSS 没变不等于对象还存活。真实服务还应排除日志、全局缓存、闭包和仍在运行的任务自己保留引用,不能把所有对象存活都归到池线程。

检查时记录实际解释器是否自由线程构建、是否启用了 GIL,以及 Trio 版本。普通解释器上的成功只能作对照,不能替代原环境。不要为了复验把敏感上下文内容打印出来,比较弱引用状态和少量受控的标记值即可。

旧的跨后端上下文值传播差异属于另一条路径。这项修复不说明 AnyIO 所有后端、阻塞门户或取消作用域问题都已经解决,也没有改变应用对任务上下文的使用约定。