Concurrency Fundamentals

Thread Safety

A type or function is thread-safe if it can be used concurrently by multiple threads—regardless of how the operating-system scheduler interleaves or parallelizes those threads—without causing data races, memory corruption, or violated type invariants.

How Rust Encodes Thread Safety in the Type System

In many languages, whether a class is thread-safe is an informal comment in documentation ("Note: ArrayList is not synchronized"). In Rust, thread safety is a first-class property of every type, separated into two distinct capabilities:

  1. Thread-safe ownership transfer (Send): Can a value of type T be moved from Thread A to Thread B?
  2. Thread-safe concurrent shared access (Sync): Can Thread A and Thread B both hold &T at the same time and call &self methods concurrently?
Thread Safety Spectrum in Rust:

  +-------------------------------------------------------------------+
  |  1. Immutable & Exclusive-Mutable Types (`i32`, `Vec<T>`, `String`)|
  |     - `Send + Sync`: Safe to share `&T` across threads because    |
  |       `&self` methods only read; mutation requires exclusive `&mut`|
  +-------------------------------------------------------------------+
  |  2. Synchronized Interior-Mutable Types (`Mutex<T>`, `AtomicU64`) |
  |     - `Send + Sync`: Safe to share `&T` across threads because    |
  |       internal mutations use atomic instructions or OS locks      |
  +-------------------------------------------------------------------+
  |  3. Unsynchronized Interior-Mutable Types (`Cell<T>`, `RefCell<T>`)|
  |     - `Send + !Sync`: Safe to move uniquely to another thread,    |
  |       FORBIDDEN to share `&T` across threads (`!Sync`)!           |
  +-------------------------------------------------------------------+
  |  4. Non-Atomic Shared Owners (`Rc<T>`) & Raw Pointers (`*mut T`)  |
  |     - `!Send + !Sync`: Confined strictly to a single thread       |
  +-------------------------------------------------------------------+

Why Ordinary Types (Vec<T>, HashMap<K, V>) Are Automatically Sync

At first glance, it can seem surprising that Vec<i32> and HashMap<String, i32> implement Sync even though they contain no internal mutexes or atomic operations.

They are Sync because every method that modifies a Vec or HashMap (.push(), .insert(), .clear()) requires an exclusive &mut self reference! If multiple threads hold shared references &Vec<i32>, the only methods they can call are read-only methods (.len(), .get(), .iter()), and concurrent reads of immutable memory are 100% thread-safe without any synchronization overhead:

use std::collections::HashMap;
use std::thread;

fn main() {
    let mut config = HashMap::new();
    config.insert("host", "127.0.0.1");
    config.insert("port", "8080");

    // Once `config` is shared via `&config`, no thread can mutate it,
    // so multiple threads can query `&HashMap` concurrently with ZERO locks:
    thread::scope(|s| {
        s.spawn(|| println!("thread 1 reads host = {:?}", config.get("host")));
        s.spawn(|| println!("thread 2 reads port = {:?}", config.get("port")));
    });
}
thread 1 reads host = Some("127.0.0.1")
thread 2 reads port = Some("8080")

Thread-Local Storage (std::thread_local!) for !Send / !Sync State

When a type is intentionally single-threaded (Rc<T>, RefCell<T>, Cell<T>) and each thread needs its own independent instance without cross-thread synchronization, the std::thread_local! macro allocates a separate copy per OS thread accessed via .with(|cell| ...) (or .get() / .set() directly on LocalKey<Cell<T>> and .with_borrow_mut() on LocalKey<RefCell<T>>):

use std::cell::Cell;
use std::thread;

thread_local! {
    static THREAD_COUNTER: Cell<u32> = const { Cell::new(0) };
}

fn main() {
    THREAD_COUNTER.set(100);

    let worker = thread::spawn(|| {
        // Worker thread has its own independent `THREAD_COUNTER` starting at 0:
        THREAD_COUNTER.set(THREAD_COUNTER.get() + 5);
        println!("worker thread counter = {}", THREAD_COUNTER.get());
    });

    worker.join().unwrap();
    println!("main thread counter   = {}", THREAD_COUNTER.get());
}
worker thread counter = 5
main thread counter   = 100