Threads

Thread Safety and Lifetimes

In concurrent Rust, thread safety (Send and Sync) and lifetimes ('static vs. 'env) are orthogonal axes that work together:

  • Send and Sync verify how a type may be accessed across threads (exclusive transfer vs. concurrent shared access).
  • Lifetimes (T: 'a) verify how long the underlying memory remains allocated relative to the threads accessing it.
Two Orthogonal Compile-Time Checks on Every Thread Spawn:

                      Lifetime Axis (How Long Memory Lives)
                     -------------------------------------
                     |  'static (Owned / Arc / &'static)  |  'env (Stack Borrow)
  -------------------+------------------------------------+----------------------
  Send + Sync        |  `thread::spawn` + `thread::scope` |  `thread::scope` ONLY
  (e.g. Vec, String) |  (Can move OR share via Arc/&'env) |  (Can borrow &'env T)
  -------------------+------------------------------------+----------------------
  Send + !Sync       |  `thread::spawn` + `thread::scope` |  `thread::scope` ONLY
  (e.g. RefCell<T>)  |  (Move uniquely only; no Arc<T>!)  |  (Move uniquely / &mut)
  -------------------+------------------------------------+----------------------
  !Send + !Sync      |  Confined to creating thread (`thread_local!` only)
  (e.g. Rc<T>)       |  (Cannot cross thread boundaries regardless of lifetime!)

Clarifying the Misconception Around T: 'static

In std::thread::spawn<F, T>(f: F) where F: FnOnce() -> T + Send + 'static, the bound F: 'static does NOT mean F must be a static global variable or live until the end of the program!

A type T satisfies T: 'static if and only if T contains no non-'static borrowed references (meaning its validity is not restricted by any shorter lexical borrow 'a):

Type Satisfies T: 'static? Why
String, Vec<u8>, u64, Box<T> Yes! Owns its heap/stack data completely; can be moved into thread::spawn and dropped at any time
Arc<Vec<u8>>, Arc<Mutex<State>> Yes! Reference-counted shared ownership; valid as long as the Arc clone lives
&'static str ("literal") Yes! Points to read-only binary segment valid for the entire program
&'a str, View<'a> (where 'a != 'static) No (E0597) Borrows from a local stack frame 'a; can only be used in std::thread::scope

Combining Owned 'static Data and Scoped 'env Borrows

use std::sync::Arc;
use std::thread;

fn main() {
    // 1. Owned `String` satisfies `'static` -> can move into `thread::spawn`
    //    and be dropped immediately when the worker finishes:
    let owned_task_name = String::from("ephemeral-owned-string");
    let spawned = thread::spawn(move || {
        println!("spawned thread owns and drops: {:?}", owned_task_name);
    });
    spawned.join().unwrap();

    // 2. Shared `Arc<T>` satisfies `'static` -> can outlive local scopes:
    let shared_config = Arc::new(String::from("shared-arc-config"));

    // 3. Local stack slice `&[u32]` has non-`'static` lifetime `'env` ->
    //    used directly inside `thread::scope` alongside `shared_config`:
    let stack_numbers = [10_u32, 20, 30];
    thread::scope(|s| {
        let cfg = Arc::clone(&shared_config);
        let nums_ref: &[u32] = &stack_numbers;
        s.spawn(move || {
            println!("scoped thread reads cfg={:?} and stack slice={:?}", cfg, nums_ref);
        });
    });
}
spawned thread owns and drops: "ephemeral-owned-string"
scoped thread reads cfg="shared-arc-config" and stack slice=[10, 20, 30]