Closures
Closures and Threading
Spawning threads in Rust passes a closure to std::thread::spawn or std::thread::scope, using trait bounds on the closure (FnOnce, Send, and 'static) to guarantee memory safety and freedom from data races at compile time.
Bounds on std::thread::spawn
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
Each bound on the closure F enforces a specific concurrency requirement:
FnOnce() -> T: The new thread executes the closure exactly once and stores its return valueTinside theJoinHandle<T>.Send: All values captured by the closure must be safe to transfer across thread boundaries (ruling outRc<T>or raw pointers).'static: Because a detached thread created bythread::spawncan outlive the function that spawned it, the closure cannot hold non-'staticreferences to the parent thread's stack frame; use amoveclosure to transfer owned data into the thread.
use std::thread;
fn main() {
let chunk = vec![10, 20, 30, 40];
// `move` transfers ownership of `chunk` into the spawned thread:
let handle = thread::spawn(move || -> i32 {
chunk.iter().sum()
});
let sum = handle.join().unwrap();
println!("Worker computed sum: {sum}");
}
Worker computed sum: 100
Borrowing Local Variables Across Threads with std::thread::scope
When threads are guaranteed to finish before the current scope exits, std::thread::scope removes the 'static requirement on the closure. Closures spawned via s.spawn(...) can capture local variables from the enclosing stack frame by shared reference (&T) or disjoint mutable reference (&mut T) without move or Arc:
use std::thread;
fn main() {
let shared_config = String::from("production-eu");
let mut results = [0_i32; 2];
let (left, right) = results.split_at_mut(1);
thread::scope(|s| {
// Both threads borrow `&shared_config` (shared) and disjoint `&mut` slices:
s.spawn(|| {
println!("Thread 1 reading {}", shared_config);
left[0] = 100;
});
s.spawn(|| {
println!("Thread 2 reading {}", shared_config);
right[0] = 200;
});
}); // All scoped threads automatically join before `scope` returns
println!("Results: {results:?} (config: {shared_config})");
}
Thread 1 reading production-eu
Thread 2 reading production-eu
Results: [100, 200] (config: production-eu)
thread::spawn vs. thread::scope
| API | Closure Bounds | Can Capture Stack References (&local)? |
Thread Join Behavior |
|---|---|---|---|
thread::spawn |
FnOnce() -> T + Send + 'static |
No — requires move / 'static data |
Manual via JoinHandle::join() (detaches if dropped) |
thread::scope (s.spawn) |
FnOnce() -> T + Send + 'scope |
Yes (when T: Sync for &T, or T: Send for &mut T) |
Automatically joins all spawned threads before scope exits |