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.
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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