thread.scope
std::thread::scope (stabilized in Rust 1.63) is the standard library function that creates a scoped thread environment &'scope Scope<'scope, 'env>, allowing threads spawned via s.spawn(...) to borrow local variables of lifetime 'env and returning the value produced by the scope body after all spawned threads have terminated.
Dual-Lifetime Signature of std::thread::scope
pub fn scope<'env, F, T>(f: F) -> T
where
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,
Understanding the two lifetimes 'env and 'scope explains how thread::scope achieves compile-time safety without garbage collection:
Lifetime Hierarchy of `std::thread::scope`:
Outer Environment Lifetime `'env` (Variables declared BEFORE `thread::scope`)
+-------------------------------------------------------------------------+
| let stack_vec = vec![10, 20, 30]; |
| |
| Inner Scope Lifetime `'scope` (Duration of `thread::scope(|s| ...)`) |
| +-----------------------------------------------------------------+ |
| | s.spawn(|| { /* can borrow `&'env stack_vec` AND `&'scope s` */ }) |
| | // Spawned threads can even spawn nested sibling threads via `s`!| |
| +-----------------------------------------------------------------+ |
| // All `'scope` threads are guaranteed joined before `'scope` ends! |
+-------------------------------------------------------------------------+
| Lifetime | What It Represents | Constraint |
|---|---|---|
'env |
Lifetime of stack variables declared outside thread::scope |
'env: 'scope ('env outlives the entire thread::scope call) |
'scope |
Higher-ranked (for<'scope>) lifetime of the thread::scope body itself |
Ends before thread::scope returns; threads can borrow 'env data AND &'scope Scope |
Returning Computed Values Directly from thread::scope
Because thread::scope returns whatever value T its closure produces (after joining any unjoined scoped threads), you can collect ScopedJoinHandle<'scope, R> results inside the scope and return the aggregated result directly:
use std::thread;
fn parallel_map<In, Out, F>(items: &[In], f: F) -> Vec<Out>
where
In: Sync,
Out: Send,
F: Fn(&In) -> Out + Sync,
{
thread::scope(|s| {
let handles: Vec<thread::ScopedJoinHandle<'_, Out>> = items
.iter()
.map(|item| s.spawn(|| f(item)))
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect()
})
}
fn main() {
let words = [String::from("rust"), String::from("scoped"), String::from("threads")];
let lengths = parallel_map(&words, |w| (w.as_str(), w.len()));
// Notice that `lengths` can even contain `&str` slices borrowing from `words`!
println!("mapped slices & lengths: {:?}", lengths);
}
mapped slices & lengths: [("rust", 4), ("scoped", 6), ("threads", 7)]
Spawning Sibling Threads Recursively via &'scope Scope
Because s is passed into thread::scope as a shared reference &'scope Scope<'scope, 'env> (and Scope implements Sync), a thread spawned inside s.spawn(...) can capture s by copy (let s = s;) and call s.spawn(...) from inside the worker thread to dynamically spawn recursive divide-and-conquer tasks (such as parallel quicksort or tree traversal) within the same scope!
use std::thread;
fn parallel_sum<'env>(slice: &'env [u64]) -> u64 {
thread::scope(|s| {
fn recurse<'scope, 'env: 'scope>(
s: &'scope thread::Scope<'scope, 'env>,
sub: &'env [u64],
) -> u64 {
if sub.len() <= 2 {
return sub.iter().sum();
}
let (left, right) = sub.split_at(sub.len() / 2);
// Spawn left half on a sibling scoped thread via `s`, compute right half on current thread:
let left_handle = s.spawn(move || recurse(s, left));
let right_sum = recurse(s, right);
left_handle.join().unwrap() + right_sum
}
recurse(s, slice)
})
}
fn main() {
let data = [10_u64, 20, 30, 40, 50, 60, 70, 80];
println!("recursive scoped sum: {}", parallel_sum(&data));
}
recursive scoped sum: 360