Scoped threads

All the lifetime issues we discussed so far have a common source: the spawned thread can outlive its parent.
We can sidestep this issue by using scoped threads.

let v = vec![1, 2, 3];
let midpoint = v.len() / 2;

std::thread::scope(|scope| {
    scope.spawn(|| {
        let first = &v[..midpoint];
        println!("Here's the first half of v: {first:?}");
    });
    scope.spawn(|| {
        let second = &v[midpoint..];
        println!("Here's the second half of v: {second:?}");
    });
});

println!("Here's v: {v:?}");

Let's unpack what's happening.

scope

The std::thread::scope function creates a new scope.
std::thread::scope takes as input a closure, with a single argument: a Scope instance.

Scoped spawns

Scope exposes a spawn method.
Unlike std::thread::spawn, all threads spawned using a Scope will be automatically joined when the scope ends.

If we were to "translate" the previous example to std::thread::spawn, it'd look like this:

let v = vec![1, 2, 3];
let midpoint = v.len() / 2;

let handle1 = std::thread::spawn(|| {
    let first = &v[..midpoint];
    println!("Here's the first half of v: {first:?}");
});
let handle2 = std::thread::spawn(|| {
    let second = &v[midpoint..];
    println!("Here's the second half of v: {second:?}");
});

handle1.join().unwrap();
handle2.join().unwrap();

println!("Here's v: {v:?}");

Borrowing from the environment

The translated example wouldn't compile, though: the compiler would complain that &v can't be used from our spawned threads since its lifetime isn't 'static.

That's not an issue with std::thread::scope—you can safely borrow from the environment.

In our example, v is created before the spawning points. It will only be dropped after scope returns. At the same time, all threads spawned inside scope are guaranteed to finish before scope returns, therefore there is no risk of having dangling references.

The compiler won't complain!

Exercise

The exercise for this section is located in 07_threads/04_scoped_threads