๐ŸŽ“ A1Learner
๐Ÿ”

Rust Scoped Threads

Scoped threads are threads that may borrow local values, the way ordinary code does. The scope guarantees every thread finishes before the scope ends.

Open a scope, spawn from its handle

Use thread::scope to open the scope, and s.spawn(...) โ€” not thread::spawn(...) โ€” for the threads inside it:

thread::scope(|s| {
    s.spawn(|| println!("working inside the scope"));
});
// the scope ends only when every thread inside it has finished

Threads may borrow โ€” nothing moves, nothing is cloned

No move, no Arc, and no clone just to please the compiler. The borrow is safe because the scope cannot end while its threads run:

use std::thread;
fn main() {
    let words = vec!["rust", "is", "fearless"];
    thread::scope(|s| {
        let h = s.spawn(|| words.len());
        println!("borrowed count = {}", h.join().unwrap());
    });
    println!("words still here = {}", words.len());
}

The scope hands a value back, too

s.spawn gives a handle. join() collects its result. That works just like a plain thread. The scope closure returns something, too. It comes out of thread::scope itself:

use std::thread;
fn main() {
    let total = thread::scope(|s| {
        let h = s.spawn(|| 20 + 22);
        h.join().unwrap()
    });
    println!("scope gave back {total}");
}

The borrowing rules still apply inside

Many threads may read the same value, but only one thread may borrow it mutably:

let mut total = 0;
thread::scope(|s| {
    s.spawn(|| total += 1);
    s.spawn(|| total += 1); // error E0499: total cannot be borrowed mutably twice at once
});

Example

use std::thread;
fn main() {
    let words = vec!["rust", "is", "fearless"];
    let total: usize = thread::scope(|s| {
        let a = s.spawn(|| words[0].len() + words[1].len());
        let b = s.spawn(|| words[2].len());
        a.join().unwrap() + b.join().unwrap()
    });
    println!("total letters = {total}");
    println!("words still here = {}", words.len());
}

Key points

  • The scope cannot end yet.
  • Every thread inside it must finish first.
  • The scope joins them all for you.
  • It even joins ones you forget to join yourself.
  • Scoped threads remove that danger: by construction, they never outlive the scope.
  • Scoped threads borrow local values because their scope waits for every thread to finish.

In this example, words stays in main โ€” nothing is moved and nothing is wrapped in an Arc. Inside the scope, two threads borrow words. They borrow at the same time. One adds the lengths of the first two words. The other takes the third. Each join collects a small sum, and the closure returns their total, which the scope hands back as total. Every thread must finish first. Only then does the scope end. So the last line can safely use words again. All 3 words are still there. Their letters total 14.

Note

Plain spawn cannot allow borrowing, because the thread might outlive the function that owns the data.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

Why may scoped threads borrow local data when plain spawn may not?