Threads
Scoped Threads
Scoped threads are OS threads whose execution lifetime is statically bounded by a lexical scope ('scope). Because the runtime guarantees that every thread spawned inside the scope finishes and joins before the scope exits, scoped threads can safely borrow non-'static local variables (&'env T and disjoint &'env mut T) directly from the parent thread's stack frame without Arc<T> or cloning!
Why Unscoped thread::spawn Cannot Borrow the Stack, While Scoped Threads Can
1. Unscoped `thread::spawn` (Requires `'static`):
Parent Function (`'env`)
| let data = vec![1, 2, 3];
| thread::spawn(|| read(&data)); <-- Rejected! Child thread could still be running
v after Parent Function returns and drops `data`!
2. Scoped `thread::scope` (Bounded by `'scope` inside `'env`):
Parent Function (`'env`)
| let data = vec![1, 2, 3];
| thread::scope(|s| { <-- Scope `'scope` begins (`'env: 'scope`)
| s.spawn(|| read(&data)); <-- Safe! Borrows `&'env data`
| s.spawn(|| read(&data));
| }); <-- BLOCKS here until ALL spawned threads exit!
| // `data` is guaranteed to still be alive and now completely unborrowed again!
v
Comparing Unscoped Threads (thread::spawn) and Scoped Threads (thread::scope)
| Property | std::thread::spawn |
std::thread::scope (s.spawn) |
|---|---|---|
| Lifetime bound on closure | F: 'static |
F: 'scope (can borrow local stack variables of lifetime 'env where 'env: 'scope) |
Can borrow &Local across threads? |
No (must move or wrap in Arc<Local>) |
Yes (any number of threads can borrow &Local simultaneously!) |
Can borrow disjoint &mut Local slices? |
No | Yes (each thread can mutate a disjoint chunks_mut slice in place!) |
What happens if JoinHandle is not explicitly .join()ed? |
Thread detaches and keeps running in background | Scope automatically joins all remaining threads before thread::scope returns! |
What happens if a worker thread panics and is not manually .join()ed? |
Panic is silently dropped with the detached handle | thread::scope propagates the panic to the parent thread after all sibling threads finish! |
Parallel Read-Plus-Disjoint-Write Without Arc or Mutex
Because the borrow checker tracks shared and mutable borrows across thread::scope, multiple worker threads can simultaneously read a shared stack lookup table (&table) while writing their outputs into disjoint mutable slices (&mut output[..]):
use std::thread;
fn main() {
let multipliers = [2_u64, 3, 5, 7];
let mut outputs = [0_u64; 4];
thread::scope(|s| {
// Borrow `multipliers` immutably (`&[u64; 4]`) across all 4 threads,
// and borrow each element of `outputs` mutably (`&mut u64`) in 1 thread:
for (idx, out_slot) in outputs.iter_mut().enumerate() {
let mult_ref = &multipliers;
s.spawn(move || {
*out_slot = mult_ref[idx] * 100 + (idx as u64);
});
}
}); // All 4 threads are automatically joined here before `outputs` is read below!
println!("outputs: {:?}", outputs);
}
outputs: [200, 301, 502, 703]