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
现象
两种表现:
- 在
main里设了一个 thread_local(日志的请求 ID、配置、分配器上下文),pool.install(|| ...)或par_iter的闭包里读到的是默认值。 - 每个任务开始时把 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 被改掉的元素 |
|---|---|
直接 set | 15、18、9(/ 200) |
set 前保存旧值,用 Drop 守卫恢复 | 0、0、0 |
直接 set,RAYON_NUM_THREADS=1(反向验证) | 0、0、0 |
三种模式的求和结果相同:计算本身没有错,错的只是「当前上下文」。
从哪来
- 读不到:
install的文档第一条警告就是这个(thread_pool/mod.rs:68-76):「Becauseopis 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 不可,就保存旧值并用守卫恢复。
💬 关联讨论
发起讨论加载中...