Can't understand rayon threadpooling

The following code looks (to me at least) like it should be multithreaded, but it is not

fn main() {
    let pool = rayon::ThreadPoolBuilder::new()
    .num_threads(16)
    .build()
    .unwrap();

    for thread in 0..10 {
        pool.install(|| {
            std::thread::sleep(std::time::Duration::from_secs(1));
            println!("Executing thread {thread}")
        });
    }
}

When running this code via time cargo run, I get this:

Executing thread 0
Executing thread 1
Executing thread 2
Executing thread 3
Executing thread 4
Executing thread 5
Executing thread 6
Executing thread 7
Executing thread 8
Executing thread 9

real    0m11.217s
user    0m2.253s
sys     0m0.272s

It took 10s to execute (1s per "thread"), so it seems like it isn't multithreaded.

However, if i replace the pool.install(|| {...}) by std::thread::spawn(move || {...}), I get this instead:


real    0m1.280s
user    0m2.376s
sys     0m0.254s

Why isn't the rayon code multithreaded ?

Furthermore, there are 2 other things that I don't understand:

  • Why did the std::thread::spawn code require a move closure while the rayon code didn't ?
  • Why didn't the std::thread::spawn code print anything ?

I feel like there is something fundamental about rayon/multithreading that I don't understand

install doesn't spawn a new thread, the threads spawned (through rayon, not directly via the std) in the scope of the closure executed by install will use the pool.

// this should do what you intended 
pool.install(|| {
    (0..10).into_par_iter().for_each(|i| {
        std::thread::sleep(...);
        println!(...);
    });
});
5 Likes

Rayon's install is also a blocking call, so when you loop over multiple install calls, it's waiting for each to complete before starting the next. Instead you could use a spawn, especially a scope-spawn to still wait for the batch of them to complete.

std::thread::spawn requires a 'static closure because it doesn't have any blocking to make sure that borrows stay alive for the duration of the thread. So if your closure is capturing any local variables by reference, those need move to transfer to the thread instead.

Rayon's spawn is like that too, but install, join, and scope (with inner spawn) are all blocking calls. They make sure that the thread calls are completed before returning, so it is safe for them to use local borrows during that time. The relatively new std::thread::scope also works like that.

Because you didn't wait for the threads to complete -- you returned from main while they were still sleeping. You can save the JoinHandle for each new thread and join them at the end, or use a scope that will do this for you.

4 Likes