Rust in Everything
基于 rayon-core 1.13.0

thread_local 在池里读不到,或者被别的任务改掉

主线位置:主线 1 的「在哪个线程上跑」。install 把闭包送到 worker 上(in_worker:从哪进池),worker 等待时执行别的任务(WorkerThread 主循环)。

sequenceDiagram
  participant W as worker W
  participant O as 另一个 worker
  W->>W: 外层元素 i:CURRENT = i
  W->>W: 内层 par_iter:join,推 B
  O->>W: 偷走 B
  W->>W: wait_until:弹出外层元素 j
  W->>W: 元素 j:CURRENT = j,做完返回
  W->>W: 回到元素 i:读 CURRENT,得到 j

现象

两种表现:

  1. 在 main 里设了一个 thread_local(日志的请求 ID、配置、分配器上下文),pool.install(|| ...) 或 par_iter 的闭包里读到的是默认值。
  2. 每个任务开始时把 thread_local 设成「当前任务的上下文」,结束前再读,偶尔发现它变成了另一个任务的值,而那个任务和自己没有任何关系。

复现

exp-rayon/pitfalls/ 的 tls(正确性实验,1 / 5 分钟负载约 5.1 / 5.5)。

读不到:main 里 CONFIG.set(7),4 线程的私有池:

install: main sees 7, install ran on worker Some(1) and sees 0, every worker: [0, 0, 0, 0]
broadcast: main sees 7, install ran on worker Some(2) and sees 7, every worker: [7, 7, 7, 7]

第二行是改法:先 pool.broadcast(|_| CONFIG.set(v)),每个 worker 上各设一次,之后 install 在哪个 worker 上都读得到。

被改掉:外层 200 个元素 par_iter().map,每个元素 CURRENT.set(i),然后跑一个 2000 个元素的内层 par_iter().sum(),回来检查 CURRENT 是否仍是 i。12 线程,每种 3 次:

模式发现 CURRENT 被改掉的元素
直接 set15、18、9(/ 200)
set 前保存旧值,用 Drop 守卫恢复0、0、0
直接 set,RAYON_NUM_THREADS=1(反向验证)0、0、0

三种模式的求和结果相同:计算本身没有错,错的只是「当前上下文」。

从哪来

  • 读不到:install 的文档第一条警告就是这个(thread_pool/mod.rs:68-76):「Because op is executing within the Rayon thread pool, thread-local data from the current thread will not be accessible.」从 main 进池走 in_worker_cold,闭包在另一个线程上执行;par_iter 的叶子更是分散在各个 worker 上。
  • 被改掉:元素 i 在内层 par_iter 里等被偷走的子任务时,当前线程进 wait_until_cold,从本地 deque 弹出外层的下一个元素 j,在同一线程上执行,j 把 CURRENT 设成 j 后返回。i 恢复执行时读到的是 j。这和 持着 Mutex 调 par_iter,偶尔卡死 是同一个机制,E7 的 detect 模式数的就是这种嵌套。1 线程时没有窃取,等待时弹出的总是自己的子任务,所以是 0。

怎么改

  • 每个 worker 初始化一次:pool.broadcast(|_| init_tls()),或者建池时用 start_handler(|index| ...)(每个线程启动时调用一次)。适合「整个池共享的配置」。
  • 借用调用方的数据:ThreadPoolBuilder::build_scoped 让 worker 线程跑在 std::thread::scope 里,可以配 scoped_tls 把栈上的数据借给每个线程,文档例子见 lib.rs:284-321(ThreadPoolBuilder)。
  • 按任务的上下文不要放在 thread_local:显式作为参数传进闭包(par_iter().map(|x| f(ctx, x)))。非放不可时,设置时保存旧值、用 Drop 守卫恢复(上面 0/200 的写法),这样嵌套执行的任务结束时会把值还回去。
  • 需要「每个任务一个累加器」时,用 fold / map_init,而不是 thread_local 加锁。

小结

  • install / par_iter 的闭包跑在 worker 上,读不到调用线程的 thread_local;broadcast 或 start_handler 给每个 worker 设一次(实测从全 0 变成全 7)。
  • 任务等待时同一线程会执行别的任务,按任务设置的 thread_local 会被改写:200 个元素里 9–18 个,1 线程时 0 个。
  • 上下文显式传参;非用 thread_local 不可,就保存旧值并用守卫恢复。

💬 关联讨论

发起讨论
加载中...