ThreadPoolBuilder
主线位置:四条主线之前的准备。池子在这里决定有几个线程、线程怎么起、panic 交给谁。线程数会直接影响主线 2 的切分(
Splitter的初值就是current_num_threads())。
字段与默认值
ThreadPoolBuilder<S = DefaultSpawn> 有九个字段,Default 全部取「空」(lib.rs:226-240):
| 字段 | 默认 | setter | 用在哪 |
|---|---|---|---|
num_threads: usize | 0(= 自动) | num_threads(n) | Registry::new 开几个线程 |
use_current_thread: bool | false | use_current_thread() | 调用线程变成 0 号 worker |
panic_handler | None | panic_handler(f) | spawn 类任务的 panic 交给谁;没有就 abort |
get_thread_name | None | thread_name(f) | 每个线程的名字,f(index) |
stack_size | None | stack_size(n) | 线程栈大小,None 用 std 默认 |
start_handler | None | start_handler(f) | worker 进入主循环前调用 f(index) |
exit_handler | None | exit_handler(f) | worker 退出主循环后调用 f(index) |
spawn_handler: S | DefaultSpawn | spawn_handler(f) | 用什么方式起线程 |
breadth_first: bool | false | breadth_first()(废弃) | 本地 deque 用 FIFO 而不是 LIFO |
三个 handler 的类型都要求 Send + Sync + 'static(lib.rs:213-223):它们存进 Registry,被所有 worker 共享。Registry::new 用 take_*_handler 把它们从 builder 里拿走。
线程数怎么定
fn get_num_threads(&self) -> usize {
if self.num_threads > 0 {
self.num_threads
} else {
let default = || {
thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
};
match env::var("RAYON_NUM_THREADS")
.ok()
.and_then(|s| usize::from_str(&s).ok())
{
Some(x @ 1..) => return x,
Some(0) => return default(),
_ => {}
}
// Support for deprecated `RAYON_RS_NUM_CPUS`.
match env::var("RAYON_RS_NUM_CPUS")
.ok()
.and_then(|s| usize::from_str(&s).ok())
{
Some(x @ 1..) => x,
_ => default(),
}
}
}
flowchart TB
n{"num_threads(n) 且 n > 0"} -->|是| use["用 n"]
n -->|否| e{"RAYON_NUM_THREADS 解析为 usize"}
e -->|"正数"| usee["用它"]
e -->|"0"| def["available_parallelism(),失败取 1"]
e -->|"未设或解析失败"| old{"RAYON_RS_NUM_CPUS 解析为正数"}
old -->|是| useo["用它"]
old -->|否| def
use --> cap["Registry::new:min(n, max_num_threads())"]
usee --> cap
useo --> cap
def --> cap
顺序:
num_threads(n)且n > 0:用 n,环境变量不看。RAYON_NUM_THREADS能按usize::from_str解析成正数:用它。解析成0:直接用默认值,不再看旧变量。- 没设、解析失败(非数字、空串、负数、带空格):落到废弃的
RAYON_RS_NUM_CPUS,同样只认正数。 - 都没有:
available_parallelism(),取不到就 1。默认值从 rayon-core 1.12.0(rayon 1.8.0)起改用available_parallelism,之前用num_cpuscrate(RELEASES.md)。
然后 Registry::new 再截一次上限(registry.rs:245-246):
// Soft-limit the number of threads that we can actually support.
let n_threads = Ord::min(builder.get_num_threads(), crate::max_num_threads());
max_num_threads() 返回 sleep::THREADS_MAX,即 sleep 计数器里每段的最大值(counters.rs:55-77):64 位平台 THREADS_BITS = 16,上限 2¹⁶ − 1 = 65535;32 位平台 8 位,上限 255。超过上限静默截断,不报错。这个软上限是 rayon-core 1.9.2(rayon 1.5.2)加的(d8e9059「Enforce THREADS_MAX and expose it with max_num_threads()」,#926);在那之前计数器的位宽限制就已存在,只是没有检查。
num_threads 的文档还有一段「Future compatibility warning」:默认行为将来可能改成动态增减线程,想要固定线程数就显式设置(lib.rs:504-528)。
实测:环境变量的解析
exp-rayon/global-init 的 env_threads 打印全局池大小和一个 num_threads(2) 池的大小(本机 available_parallelism() = 12,max_num_threads() = 65535):
RAYON_NUM_THREADS | RAYON_RS_NUM_CPUS | 全局池 | num_threads(2) 的池 |
|---|---|---|---|
| 未设 | 未设 | 12 | 2 |
4 | 未设 | 4 | 2 |
0 | 未设 | 12 | 2 |
abc | 未设 | 12 | 2 |
" 4"(前导空格) | 未设 | 12 | 2 |
-1 | 未设 | 12 | 2 |
"" | 未设 | 12 | 2 |
| 未设 | 3 | 3 | 2 |
5 | 3 | 5 | 2 |
0 | 3 | 12 | 2 |
abc | 3 | 3 | 2 |
最后两行是代码顺序的直接结果:0 在第一个 match 里就 return default(),旧变量没有机会生效;abc 解析失败,落到第二个 match,旧变量生效。非数字的值没有任何提示,是静默忽略的。反向验证:num_threads(2) 的池在所有组合下都是 2,说明环境变量只在 num_threads 为 0 时才被读。
一次意外的观察。 测试 RAYON_NUM_THREADS=100000 时,这个值被截到 65535,Registry::new 开始逐个起线程,起到一部分时 thread::Builder::spawn 返回 EAGAIN(macOS 的每进程线程数上限),进程 panic:
The global thread pool has not been initialized.: ThreadPoolBuildError { kind: IOError(Os { code: 35, kind: WouldBlock, message: "Resource temporarily unavailable" }) }
这条消息来自 global_registry() 的 .expect(registry.rs:171):全局池是被第一次 rayon 调用隐式创建的,创建失败没有地方返回 Err,只能 panic。已起的线程由 Terminator 守卫在 Registry::new 提前返回时终止(全局池与 Registry)。这次运行期间本机 1 分钟负载一度升到 400 以上,所以只跑了一次,没有重复,也不建议复现。结论只有一条:截断到 65535 不等于「65535 个线程能起来」。
build、build_global、build_scoped
build → ThreadPool::build → Registry::new,返回 ThreadPool{registry}。它不等线程启动完:Registry::new 起完线程就返回。
build_global 走 registry::init_global_registry,成功后还要 wait_until_primed():等每个线程的 primed latch 被 set,即所有 worker 都已进入主循环。文档说调它只有两个理由:改默认配置,或者跑基准时让线程先就位。全局池只能初始化一次,第二次返回 GlobalPoolAlreadyInitialized,机制和实测见 全局池与 Registry。
build_scoped 是一个组合:在 std::thread::scope 里,用 spawn_handler 让每个 worker 都是 scoped 线程,每个线程先执行用户的 wrapper(thread)(期望它最终调 thread.run()),然后把池借给 with_pool(&pool):
std::thread::scope(|scope| {
let pool = self
.spawn_handler(|thread| {
let mut builder = std::thread::Builder::new();
if let Some(name) = thread.name() {
builder = builder.name(name.to_string());
}
if let Some(size) = thread.stack_size() {
builder = builder.stack_size(size);
}
builder.spawn_scoped(scope, || wrapper(thread))?;
Ok(())
})
.build()?;
Ok(with_pool(&pool))
})
with_pool 返回时 pool 被 drop(发出终止信号),thread::scope 再等所有 worker 退出才返回。所以 worker 可以借用 scope 外面的非 'static 数据,文档的例子是用 scoped_tls 给每个 worker 设一个线程局部值。build_scoped 在 rayon-core 1.5.0(rayon 1.1.0)随 spawn_handler 一起加入,当时用 crossbeam 的 scope;1.12.0(rayon 1.8.0)改用 std::thread::scope(RELEASES.md)。它只定义在 ThreadPoolBuilder<DefaultSpawn> 上(lib.rs:284),因为它自己要设置 spawn handler。
spawn_handler 与 ThreadSpawn
起线程这件事被抽成一个 trait(registry.rs:65-75):
pub trait ThreadSpawn {
private_decl! {}
/// Spawn a thread with the `ThreadBuilder` parameters, and then
/// call `ThreadBuilder::run()`.
fn spawn(&mut self, thread: ThreadBuilder) -> io::Result<()>;
}
private_decl! 往 trait 里加一个参数类型不可命名的方法,crate 外无法实现它(private.rs)。注释说它是「pub-in-private」:因为出现在公开类型 ThreadPoolBuilder<S> 的 bound 里,不得不 pub。两个实现:
DefaultSpawn:thread::Builder设名字和栈大小,b.spawn(|| thread.run())(registry.rs:84-98)。CustomSpawn<F>:直接调用户的F: FnMut(ThreadBuilder) -> io::Result<()>。
spawn_handler(f) 返回的是另一个类型 ThreadPoolBuilder<CustomSpawn<F>>,逐个字段搬过去(lib.rs:433-449,注释写着 // ..self,因为类型参数变了不能用结构体更新语法)。用户拿到的 ThreadBuilder 带着 index()、name()、stack_size() 和私有的 worker / stealer / registry;run() 就是 main_loop(self),在池被 drop 之前不会返回(registry.rs:47-51)。spawn handler 返回 Err 时,Registry::new 返回 ThreadPoolBuildError::IOError。
文档提醒:线程在池 drop 之后才退出,调用方需要自己等;scoped 线程会在 scope 结束时被 join,如果池被泄漏就会永远阻塞;全局池直到进程退出都不终止。
use_current_thread
rayon-core 1.12.0(rayon 1.8.0)加入(9461f7b,#1063)。Registry::new 起线程的循环里,0 号线程特殊处理(registry.rs:295-311):
if index == 0 && builder.use_current_thread {
if !WorkerThread::current().is_null() {
return Err(ThreadPoolBuildError::new(
ErrorKind::CurrentThreadAlreadyInPool,
));
}
// Rather than starting a new thread, we're just taking over the current thread
// *without* running the main loop, so we can still return from here.
// The WorkerThread is leaked, but we never shutdown the global pool anyway.
let worker_thread = Box::into_raw(Box::new(WorkerThread::from(thread)));
unsafe {
WorkerThread::set_current(worker_thread);
Latch::set(®istry.thread_infos[index].primed);
}
continue;
}
当前线程不起新线程,而是把一个堆上、故意泄漏的 WorkerThread 设成线程本地的「当前 worker」,然后继续往下起 1 号以后的线程。它不跑主循环,所以:不会主动去偷别人的任务,也不会执行 start_handler / exit_handler;只有当它自己调用 rayon(join、scope、yield_now…)并进入等待时,才会顺便执行池里的任务。文档说「the spawn and exit handlers do not run for that thread」,实测 start_handler 也不会为它运行(它只在 main_loop 里调用)。用在局部池上时 registry 会泄漏。
实测(exp-rayon/global-init 的 use_current,3 线程池,带计数的 start_handler):
start_handler(2)
start_handler(1)
start_handler calls=2
main: pool.current_thread_index()=Some(0) rayon::current_thread_index()=Some(0)
main: rayon::current_num_threads()=3
rayon::join from main -> indices (Some(0), Some(0))
second use_current_thread pool -> Err("The current thread is already part of another thread pool.")
plain thread: current_thread_index()=None
start_handler只跑了 1、2 两次。main成了 0 号 worker 之后,全局函数rayon::current_num_threads()返回 3 而不是全局池的 12:in_worker和Registry::current()只看线程本地的「当前 worker」,不区分是不是全局池(in_worker:从哪进池)。从此main上的rayon::join、par_iter都在这个 3 线程池里跑,而且main自己是参与者:这次join两边都在 0 号上执行(B 没被偷,自己弹回来了)。- 同一线程再建一个
use_current_thread池,返回CurrentThreadAlreadyInPool,Display是「The current thread is already part of another thread pool.」。 - 反向:普通线程上
current_thread_index()是None。
全局池在不支持线程的平台(wasm)上的回退,就是用 num_threads(1).use_current_thread() 建的,见 全局池与 Registry。
几个 handler
panic_handler:rayon 能把 panic 传回调用方时(join、scope、install)就传回去;spawn这类没有调用方可传的,交给它。文档:「If no panic handler is set, the default is to abort the process」;handler 自己 panic 也 abort(lib.rs:557-577)。实现是Registry::catch_unwind(registry.rs:375-384),细节和 E11 在 spawn 与 spawn_fifo。start_handler/exit_handler:在 worker 线程上、主循环前后调用,也经catch_unwind包着,panic 同样交给panic_handler或 abort。常见用途:设置线程优先级、注册线程局部的分配器或 profiler。candle 的私有池的工作线程在 macOS 上启动时调pthread_set_qos_class_self_np,让系统优先把它们放在性能核上(candle 卷 CPU 后端;那一页没有说是经哪个 hook 调的)。thread_name:FnMut(usize) -> String,tantivy 的搜索池用它起名tantivy-search-{i}(tantivy 卷)。
废弃的 API
Configuration(旧的 builder,lib.rs:205-209,内部就是包了一个 ThreadPoolBuilder)、initialize(config)(= build_global)、ThreadPool::new(config) 在 rayon 1.0.0 / rayon-core 1.4.0 时废弃(RELEASES.md:「The new ThreadPoolBuilder replaces and deprecates Configuration」),rayon 本体从那时起不再重新导出它们;breadth_first() 在 rayon 1.1.0 / rayon-core 1.5.0 废弃,建议改用 scope_fifo / spawn_fifo。RAYON_RS_NUM_CPUS 在 rayon 0.8.0 / rayon-core 1.1.0 时被 RAYON_NUM_THREADS 取代,仍然支持。
小结
- 九个字段,默认全空;三个 handler 要求
Send + Sync + 'static,存进Registry共享。 - 线程数:
num_threads(n>0)→RAYON_NUM_THREADS(0等于默认值,非数字静默忽略并落到旧变量)→RAYON_RS_NUM_CPUS→available_parallelism();再静默截到max_num_threads()(64 位 65535)。 build不等线程就位,build_global等;build_scoped=std::thread::scope+spawn_handler。use_current_thread让调用线程成为 0 号 worker 但不跑主循环,不调start_handler;之后这个线程上的所有全局 rayon 调用都进这个池。spawn的 panic 没有panic_handler就 abort。