WorkerThread 主循环
主线位置:主线 1 的「B 被偷走,原线程边等边找活」和主线 3 的入口。worker 的主循环和
join等 B 用的是同一个函数wait_until,区别只是等的 latch 不同。
WorkerThread
pub(super) struct WorkerThread {
/// the "worker" half of our local deque
worker: Worker<JobRef>,
/// the "stealer" half of the worker's broadcast deque
stealer: Stealer<JobRef>,
/// local queue used for `spawn_fifo` indirection
fifo: JobFifo,
index: usize,
/// A weak random number generator.
rng: XorShift64Star,
registry: Arc<Registry>,
}
| 字段 | 说明 |
|---|---|
worker | 本地 deque 的主人端(!Sync,只能本线程用),push / pop |
stealer | 本线程广播队列的窃取端,不是本地 deque 的;本地 deque 的 Stealer 在 Registry::thread_infos[index] 里给别人用 |
fifo | spawn_fifo 的间接层,JobRef 与几种 Job |
index | 0..num_threads,current_thread_index() 返回的就是它 |
rng | 选窃取起点用的 xorshift* |
registry | 所属的池 |
「当前线程是哪个池的哪个 worker」存在一个线程本地裸指针里(registry.rs:665-672):
// This is a bit sketchy, but basically: the WorkerThread is
// allocated on the stack of the worker on entry and stored into this
// thread-local variable. So it will remain valid at least until the
// worker is fully unwound. Using an unsafe pointer avoids the need
// for a RefCell<T> etc.
thread_local! {
static WORKER_THREAD_STATE: Cell<*const WorkerThread> = const { Cell::new(ptr::null()) };
}
const 初始化让读取不需要懒初始化检查,WorkerThread::current() 就是一次线程本地读。set_current 断言原来是 null(一个线程只能属于一个池),WorkerThread 的 Drop 把它恢复成 null(registry.rs:687-713)。rayon-core 里几乎所有「我在哪」的判断都从这个指针出发:in_worker、inject_or_push、Registry::current、current_thread_index。
main_loop
unsafe fn main_loop(thread: ThreadBuilder) {
let worker_thread = &WorkerThread::from(thread);
WorkerThread::set_current(worker_thread);
let registry = &*worker_thread.registry;
let index = worker_thread.index;
// let registry know we are ready to do work
Latch::set(®istry.thread_infos[index].primed);
// Worker threads should not panic. If they do, just abort, as the
// internal state of the thread pool is corrupted. Note that if
// **user code** panics, we should catch that and redirect.
let abort_guard = unwind::AbortIfPanic;
// Inform a user callback that we started a thread.
if let Some(ref handler) = registry.start_handler {
registry.catch_unwind(|| handler(index));
}
worker_thread.wait_until_out_of_work();
// Normal termination, do not abort.
mem::forget(abort_guard);
// Inform a user callback that we exited a thread.
if let Some(ref handler) = registry.exit_handler {
registry.catch_unwind(|| handler(index));
// We're already exiting the thread, there's nothing else to do.
}
}
WorkerThread放在本函数的栈上(&WorkerThread::from(..)的临时值生命周期延长到函数结束),地址写进线程本地。- set
primed:build_global在等的就是它。 - 装上
AbortIfPanic:之后到正常退出前,任何越过这里的 unwind 都会在守卫析构时打印「Rayon: detected unexpected panic; aborting」并abort()(unwind.rs:24-31)。用户代码的 panic 都在更里层被catch_unwind接住,能走到这里的只有 rayon 自己的 bug。 start_handler。wait_until_out_of_work:self.wait_until(®istry.thread_infos[index].terminate),然后 setstopped。
所以「主循环」并不是一个单独写的循环:worker 的一生就是等自己的 terminate latch,等的方式和 join 等 B 完全一样,在等待中执行找到的一切任务。
找活顺序
fn find_work(&self) -> Option<JobRef> {
// Try to find some work to do. We give preference first
// to things in our local deque, then in other workers
// deques, and finally to injected jobs from the
// outside. The idea is to finish what we started before
// we take on something new.
self.take_local_job()
.or_else(|| self.steal())
.or_else(|| self.registry.pop_injected_job())
}
展开成四步:
flowchart LR a["1 本地 deque:worker.pop()"] -->|空| b["2 本线程广播队列:stealer.steal()"] b -->|空| c["3 随机起点轮询其他线程的本地 deque"] c -->|都空且无 Retry| d["4 全局注入队列 injected_jobs"] d -->|空| e["交给 sleep:no_work_found"]
- 本地 deque:
worker.pop(),LIFO 下取最新推进去的任务(通常是自己刚join出去的 B)。 - 本线程的广播队列:
take_local_job在本地弹不出来时,接着从自己的广播队列steal,Retry就原地再试。广播任务只能由这个线程执行,所以排在偷别人之前。 - 偷其他线程:
steal(),见下一节。 - 全局注入队列:
pop_injected_job,Injector::steal循环到Success或Empty。
注释给的理由是「finish what we started before we take on something new」:已经在池里展开的计算(别人 deque 里的子任务)优先于外面新来的请求(install、池外的 join、spawn)。后果是:池子忙的时候,新注入的任务要等所有已展开的计算都被吃完才轮得到。crossbeam 卷 Injector 一页从队列的角度提过这一点。
steal:随机起点,Retry 整轮重来
WorkerThread::steal 的代码和 Steal 三态的语义,crossbeam 卷 Steal、Retry 与批量窃取 已经逐行讲过:从 rng.next_usize(num_threads) 开始绕一圈,跳过自己,第一个 Success 就返回;一圈都没偷到但遇到过 Retry,再绕一圈;没有 Retry 就返回 None。这里补三点 crossbeam 卷没讲的:
- 开头的两行:
debug_assert!(self.local_deque_is_empty()),只有本地没活才该来偷;num_threads <= 1直接返回None,单线程池不做无意义的轮询。 - 为什么
Retry要整轮重来而不是对同一个受害者原地重试:这是 2020 年的一次微优化,2e88929「Micro-optimize the WorkerThread::steal loop」(与 #785、#790、#791 一起合并,a0e5833)。提交说明:「This lifts up the loop forSteal::Retry, so we continue with other threads before trying again」,大部分基准变化在噪声内,join_microbench::increment_all_max最多快 8%。直觉是:Retry说明有人在和你抢同一个队列,与其挤在那里,不如先看看别的队列。 - 随机数:
XorShift64Star,种子是一个全局AtomicUsize计数器fetch_add(1)后的DefaultHasher哈希,循环直到非零。每个 worker 的种子不同,不需要系统随机源。这是 2018 年b0b6c13「Implement our own PRNG for rayon-core」引入的。
rayon 只用单个 steal(),不用 steal_batch。
wait_until_cold:双层循环
#[inline]
pub(super) unsafe fn wait_until<L: AsCoreLatch + ?Sized>(&self, latch: &L) {
let latch = latch.as_core_latch();
if !latch.probe() {
self.wait_until_cold(latch);
}
}
#[cold]
unsafe fn wait_until_cold(&self, latch: &CoreLatch) {
// the code below should swallow all panics and hence never
// unwind; but if something does wrong, we want to abort,
// because otherwise other code in rayon may assume that the
// latch has been signaled, and that can lead to random memory
// accesses, which would be *very bad*
let abort_guard = unwind::AbortIfPanic;
'outer: while !latch.probe() {
// Check for local work *before* we start marking ourself idle,
// especially to avoid modifying shared sleep state.
if let Some(job) = self.take_local_job() {
self.execute(job);
continue;
}
let mut idle_state = self.registry.sleep.start_looking(self.index);
while !latch.probe() {
if let Some(job) = self.find_work() {
self.registry.sleep.work_found();
self.execute(job);
// The job might have injected local work, so go back to the outer loop.
continue 'outer;
} else {
self.registry
.sleep
.no_work_found(&mut idle_state, latch, || self.has_injected_job())
}
}
// If we were sleepy, we are not anymore. We "found work" --
// whatever the surrounding thread was doing before it had to wait.
self.registry.sleep.work_found();
break;
}
mem::forget(abort_guard); // successful execution, do not abort
}
- 快路径:
wait_until是#[inline]的,只做一次probe()(load(Acquire));已经 set 就不进冷函数。 - 外层:每轮先看本地。有本地活就直接执行,不碰共享的 sleep 状态(注释特意强调)——本地有活的线程不应该被算作 idle。
- 内层:本地没活了,才
start_looking(inactive 计数 +1),然后反复find_work。找到就work_found(inactive -1,必要时叫醒别人)、执行,然后continue 'outer:刚执行的任务可能又往本地推了任务(比如它自己做了join),所以回到外层先看本地。找不到就no_work_found,由 sleep 模块决定是yield_now还是去睡(一个线程睡下又醒来)。 - latch 被 set:内层循环条件不满足,
work_found把自己从 inactive 里减掉,退出。 - 整个函数被
AbortIfPanic包着:执行的任务都是JobRef::execute,各种 Job 的execute内部都已catch_unwind;若仍有 panic 穿出来,说明 rayon 内部状态已不可信,与其让调用方以为 latch 已 set 继续访问内存,不如直接 abort。
has_injected_job 是睡前最后一次检查用的闭包:本线程广播队列非空,或全局注入队列非空(registry.rs:765-767)。前一半是 rayon-core 1.10.1 修一个广播竞态时加的(be12615),见 broadcast。
等待时执行的任务从哪来
wait_until_cold 会执行它找到的任何任务:本地的、广播的、偷来的、注入的。对 worker 主循环来说这理所当然;对 join 等 B 来说,就意味着:
- 线程 W0 在
join里等 B 时,可能偷来另一个与当前调用栈毫无关系的任务,在 W0 的栈上、在当前join的栈帧之上执行它。 - 这个任务若也去拿 W0 正持有的锁,就是同一线程重入同一把
std::sync::Mutex:死锁或 panic。 - 依赖线程局部变量的代码,也会在「不属于它的」调用栈上看到前一个任务留下的值。
规划阶段的实验(E7,exp-rayon/survey 的 lock_reentry)用线程局部的深度计数做了确定性的检测:外层 200 个元素的 for_each、每个元素里再跑一个 2000 元素的 par_iter().sum(),不加锁,统计「外层任务在另一个外层任务的栈上开始执行」的次数。规划时一次运行是 48 次;写作时重跑 3 次是 27、20、87 次,每次不同,但从没有 0。反向验证:RAYON_NUM_THREADS=1 下 3 次都是 0。只有一个线程时没有窃取,等 B 时从本地弹出来的总是自己刚推的子任务,不会执行别的外层元素。持锁版本的死锁复现依赖时序,放在 09 常见坑。等待时干活换来的是:嵌套多深的 join 都不会耗尽线程、不需要为等待单独开线程。这条取舍在 08 设计取舍 汇总。
yield_now 与 yield_local
registry.rs:844-862:yield_now 就是 find_work 一次、找到就执行;yield_local 只看 take_local_job。它们把「主循环里的一步」暴露给用户,在 rayon 任务里需要轮询等待某个条件时用,避免空转。细节在 yield_now 与 yield_local。
小结
WorkerThread在 worker 栈上,地址存进const初始化的线程本地Cell<*const WorkerThread>;它的stealer是本线程广播队列的,本地 deque 的Stealer在Registry里。main_loop= setprimed→start_handler→wait_until(terminate)→exit_handler,外面一个AbortIfPanic。- 找活:本地 deque → 本线程广播队列 → 随机起点偷其他线程(
Retry整轮重来,num_threads <= 1不偷)→ 全局注入队列。外面新来的任务排在最后。 wait_until_cold:外层先看本地且不碰 sleep 状态,内层find_work/no_work_found;执行任务后回外层。- 等待时会执行任意任务,这让嵌套
join不会耗尽线程,也让持锁调 rayon、依赖 TLS 的代码出问题(E7:12 线程每次运行 20–87 次跨任务重入,1 线程为 0)。