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:

  1. FnOnce() -> T: The new thread executes the closure exactly once and stores its return value T inside the JoinHandle<T>.
  2. Send: All values captured by the closure must be safe to transfer across thread boundaries (ruling out Rc<T> or raw pointers).
  3. 'static: Because a detached thread created by thread::spawn can outlive the function that spawned it, the closure cannot hold non-'static references to the parent thread's stack frame; use a move closure 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