在线程里改 Trio CancelScope.shield,为什么报 must be called from async context
Trio 工作线程不能直接修改依赖运行循环的取消作用域属性。通过 from_thread.run_sync 返回对应 Trio 线程,并管理 token 与线程退出。
trio.to_thread.run_sync() 启动的同步函数拿到了取消作用域,尝试执行 scope.shield = False,却报 RuntimeError: must be called from async context。对象就在手里,不代表它的属性可以从任意线程修改。
Trio 0.34.0 的 shield setter 会重新计算取消状态;deadline 的更新也涉及运行循环。它们不是普通数据字段。线程需要通过 Trio 提供的入口,把这项操作交回拥有该作用域的 Trio 线程。
把属性修改送回 Trio 线程
若工作线程确实由 trio.to_thread.run_sync 创建,Trio 会传递重入所需的 token。同步函数里可以调用:
import trio
def worker(scope):
def remove_shield():
scope.shield = False
trio.from_thread.run_sync(remove_shield)
# 继续现有同步工作;取消行为由原调用方与工作函数约定。
这段只展示线程交回属性修改的位置。scope 必须仍属于正在运行的 Trio 生命周期;不能在作用域已退出、trio.run 已结束后继续使用。remove_shield 必须是短小的同步函数,不能在里面执行阻塞 I/O,也不能传入 async 函数给这个同步入口。
from_thread.run_sync 会等待 Trio 线程处理完回调,再把返回值或异常交回工作线程。不要从 Trio 线程自己调用它,否则可能直接报错;在异步函数里本来就可以按生命周期约束修改作用域,无需绕到线程再回来。
外部线程要显式携带对应 token
如果线程由其他库或 threading.Thread 创建,不能假定它继承了 Trio 的上下文。应在目标 trio.run 内先取得对应 token,再传给线程,并在重入时显式指定:
# 这行位于目标 Trio 运行上下文内。
trio_token = trio.lowlevel.current_trio_token()
# 下面这行位于外部工作线程;scope 与 trio_token 均由调用方传入。
trio.from_thread.run_sync(remove_shield, trio_token=trio_token)
remove_shield 仍是上文那种操作目标作用域的同步回调。这里只展示两个位置,不能把第二段放在 Trio 线程原地执行。一个程序有多个 trio.run 生命周期时,token 必须对应这一次运行,不能全局缓存后跨运行复用。
运行已经结束时可能得到 RunFinishedError;没有可推断 token 时也可能报 RuntimeError。这些异常应按原线程退出与任务生命周期处理,不能不断重试直到某个新运行恰好启动。
取消屏蔽变化不等于强制结束线程
把 shield 改为 false,只是允许外层取消沿着相应作用域传播。同步工作线程没有因为属性变化就自动收到能够打断任意系统调用的异常。是否等待线程完成、是否允许放弃等待、线程内如何协作停止,仍由 to_thread.run_sync 的参数和实际工作函数决定。
尤其不要让 Trio 线程同步等待工作线程退出,而工作线程又在等 from_thread.run_sync 回调完成。这个相互等待会阻塞重入。线程关闭、作用域退出和回调提交需要有明确先后关系。
保存原来的取消与线程关闭逻辑,在隔离任务中检查正常完成、外层取消、回调本身失败以及程序结束时仍有外部线程的情况。成功标准是属性修改在正确线程执行、异常有人收取、工作线程按约定结束。若回退重入调整,应一起恢复对应的线程退出流程,不要单独留下跨生命周期的 scope 或 token。


