Scoped Threads
In the previous section, we explored the spawn
method in the thread
module
and noted its limitation: it cannot borrow non-'static
data because the
compiler cannot guarantee that all threads will be joined before the lifetimes
of any borrowed values expire. To address this issue, the thread
module also
offers the scope
function, which works in conjunction with Scope
. This
combination allows for borrowing non-'static
data by ensuring that all spawned
threads within the scope are automatically joined before the function in which
they were created returns, unless they were manually joined earlier.
The scope
Function
Here's the signature for the scope
function:
pub fn scope<'env, F, T>(f: F) -> T
where
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,
The scope
function takes a closure (f
) as an argument, which receives a
Scope
object (created by scope
). This Scope
object allows threads to be
spawned using the spawn
method. The spawn method returns a
ScopedJoinHandle
, which, as the name suggests, provides a join
method to
wait for the spawned thread to complete.
Scoped threads involve two lifetimes: 'scope
and 'env
:
'env
: This is a lifetime parameter that represents the lifetime of the environment data that theScope
can borrow (meaning the data from outside the scope). It ensures that any references to the environment data outlive the scope.'scope
: This is another lifetime parameter that represents the lifetime of the scope itself. This is the period during which new scoped threads can be spawned and running. It ensures that any running threads are joined before the lifetime ends.
In plain English, the scope
function takes a closure f
that can borrow data
with a specific lifetime 'env
and ensures that all threads spawned within the
scope are joined before the function returns.
Let's revisit one of the examples from the previous section that failed to compile to better understand these concepts. Review the following code snippet and then run the program.
fn main() { use std::thread; let error = String::from("E0373"); // Compiler error E0373 thread::scope(|s| { let handler = s.spawn(|| { println!("{error}"); }); s.spawn(|| { println!("{error}"); }); handler.join().unwrap(); // Manually join the first thread // Second thread is automatically joined when the closure returns. }); }
Through these ownership and type system tools, it is guaranteed that all threads
created within scope
are joined before their lifetimes end. This allows the
Rust compiler to be certain that all borrowed data (in this case, the error
String
) remains valid for the lifetime of the threads. This is how Rust turns
many runtime errors into compile-time errors. Concepts like this facilitate
fearless concurrency in Rust!