Rust in Everything
基于 rayon-core 1.13.0

ThreadPoolBuilder

主线位置:四条主线之前的准备。池子在这里决定有几个线程、线程怎么起、panic 交给谁。线程数会直接影响主线 2 的切分(Splitter 的初值就是 current_num_threads())。

字段与默认值

ThreadPoolBuilder<S = DefaultSpawn> 有九个字段,Default 全部取「空」(lib.rs:226-240):

字段默认setter用在哪
num_threads: usize0(= 自动)num_threads(n)Registry::new 开几个线程
use_current_thread: boolfalseuse_current_thread()调用线程变成 0 号 worker
panic_handlerNonepanic_handler(f)spawn 类任务的 panic 交给谁;没有就 abort
get_thread_nameNonethread_name(f)每个线程的名字,f(index)
stack_sizeNonestack_size(n)线程栈大小,None 用 std 默认
start_handlerNonestart_handler(f)worker 进入主循环前调用 f(index)
exit_handlerNoneexit_handler(f)worker 退出主循环后调用 f(index)
spawn_handler: SDefaultSpawnspawn_handler(f)用什么方式起线程
breadth_first: boolfalsebreadth_first()(废弃)本地 deque 用 FIFO 而不是 LIFO

三个 handler 的类型都要求 Send + Sync + 'static(lib.rs:213-223):它们存进 Registry,被所有 worker 共享。Registry::new 用 take_*_handler 把它们从 builder 里拿走。

线程数怎么定

get_num_threads:

    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

顺序:

  1. num_threads(n) 且 n > 0:用 n,环境变量不看。
  2. RAYON_NUM_THREADS 能按 usize::from_str 解析成正数:用它。解析成 0:直接用默认值,不再看旧变量。
  3. 没设、解析失败(非数字、空串、负数、带空格):落到废弃的 RAYON_RS_NUM_CPUS,同样只认正数。
  4. 都没有:available_parallelism(),取不到就 1。默认值从 rayon-core 1.12.0(rayon 1.8.0)起改用 available_parallelism,之前用 num_cpus crate(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_THREADSRAYON_RS_NUM_CPUS全局池num_threads(2) 的池
未设未设122
4未设42
0未设122
abc未设122
" 4"(前导空格)未设122
-1未设122
""未设122
未设332
5352
03122
abc332

最后两行是代码顺序的直接结果: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(&registry.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。

💬 关联讨论

发起讨论
加载中...