Rust's Concurrency Model
Rust's concurrency model—often called fearless concurrency—enforces thread safety at compile time using the exact same ownership, borrowing, and type-system rules that prevent dangling pointers and aliasing bugs in single-threaded code, while keeping the language runtime minimal (1:1 native OS threads in std and zero-allocation state machines for async).
Language-Level Foundations vs. Library-Level Primitives
Rather than baking a heavy green-thread runtime or garbage collector into the language binary, Rust puts only a tiny set of concurrency rules in the core language and implements the rest in the standard library and ecosystem crates:
+-------------------------------------------------------------------+
| Language Core (Enforced by `rustc` Type & Borrow Checker) |
| - Ownership & Move semantics (`move ||`) |
| - Aliasing invariant (`many &T` XOR `one &mut T`) |
| - Auto-traits `Send` and `Sync` |
| - Lifetimes (`'static` on `thread::spawn`, `'env` on `scope`) |
| - `async` / `.await` state-machine lowering (`Future` trait) |
+---------------------------------+---------------------------------+
|
v
+-------------------------------------------------------------------+
| Standard Library (`std::thread`, `std::sync`) & Ecosystem |
| - 1:1 OS Threads (`thread::spawn`, `thread::scope`) |
| - Shared Ownership (`Arc<T>`, `Weak<T>`) |
| - Synchronization (`Mutex<T>`, `RwLock<T>`, `Condvar`, `Barrier`)|
| - Lock-Free Atomics (`AtomicBool`, `AtomicUsize`, `Ordering`) |
| - Message Channels (`std::sync::mpsc`) |
+-------------------------------------------------------------------+
Why 1:1 OS Threads Are Used in the Standard Library
Early pre-1.0 versions of Rust experimented with an M:N green-threading runtime, which required every binary to bundle a heavy scheduler and complicated C FFI stack management. Rust 1.0 and all subsequent editions standardized on 1:1 native OS threads in std::thread:
- Every
std::thread::spawnmaps directly to one operating-system thread (pthreadon Unix, Win32 thread on Windows) with zero runtime overhead when threads are not used and seamless zero-cost C FFI interoperability. - When an application needs lightweight
M:Ntask multiplexing for massive I/O concurrency, it opts intoasync/.awaitwith an executor crate (tokio,smol,embassy) tailored to its workload.
How Ownership Prevents Classic Concurrency Bugs at Compile Time
| Classic Concurrency Bug in C/C++ | How Rust Prevents It at Compile Time |
|---|---|
| Use-after-free by a background thread (main thread frees stack frame while background thread holds pointer) | thread::spawn requires F: 'static (forcing move or Arc<T>), or thread::scope joins all threads before the stack frame exits (E0373 / E0597) |
| Unprotected concurrent mutation (Data Race) | Shared references &T are immutable; mutating through &T requires a Sync wrapper (Mutex<T>, RwLock<T>, Atomic*) (E0596 / E0277) |
| Accessing a mutex-protected variable without locking | Mutex<T> owns T inside the lock; T can only be accessed through the MutexGuard<'_, T> returned by .lock() |
| Forgetting to unlock a mutex on early return | MutexGuard<'_, T> unlocks automatically in its Drop destructor |
Sending a non-thread-safe handle (Rc<T>, RefCell<T>) across threads |
Rejected by Send / Sync trait bounds on thread::spawn and thread::scope (E0277) |
use std::thread;
fn main() {
let mut results = [0_u32; 4];
// `thread::scope` + `chunks_mut` lets 4 threads mutate disjoint slices
// of a stack array in parallel with ZERO locks and ZERO heap allocations!
thread::scope(|s| {
for (idx, slot) in results.iter_mut().enumerate() {
s.spawn(move || {
*slot = ((idx as u32) + 1) * 10;
});
}
});
println!("parallel disjoint mutations: {:?}", results);
}
parallel disjoint mutations: [10, 20, 30, 40]