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:
- Thread-safe ownership transfer (
Send): Can a value of typeTbe moved from Thread A to Thread B? - Thread-safe concurrent shared access (
Sync): Can Thread A and Thread B both hold&Tat the same time and call&selfmethods 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