Synchronization Fundamentals
Synchronization coordinates the ordering of memory accesses and execution steps across threads so that invariants hold and writes performed by one thread become deterministically visible to other threads (happens-before relationships).
Why Synchronization Is Needed at Both the CPU and Compiler Levels
Even when two threads run sequentially in wall-clock time, without explicit synchronization:
- The compiler may reorder loads and stores or keep a variable cached in a CPU register across a loop if it sees no synchronization barrier.
- The CPU hardware buffers writes in per-core store buffers and executes instructions out of order, meaning Core 1 can observe Core 0's writes in a different order than Core 0 issued them!
Synchronization primitives establish a happens-before edge: every memory write performed by Thread A before a release operation (unlocking a Mutex, sending on a channel, or storing with Ordering::Release) is guaranteed to be visible to Thread B after Thread B performs the matching acquire operation (locking the same Mutex, receiving from the channel, or loading with Ordering::Acquire).
Happens-Before Memory Visibility Edge:
Thread A Thread B
-------- --------
1. Write `payload = 42`
2. Release (`mutex.unlock()` / 3. Acquire (`mutex.lock()` /
`flag.store(true, Release)`) `flag.load(Acquire) == true`)
\ /
+--- [ Happens-Before ] --+
|
v
4. Read `payload` -> Guaranteed to see `42`!
Overview of Standard Library Synchronization Primitives (std::sync)
| Primitive | Purpose | Blocking or Lock-Free? |
|---|---|---|
AtomicBool, AtomicUsize, AtomicU64, AtomicPtr |
Lock-free hardware atomic loads, stores, swaps, and read-modify-write (fetch_add, compare_exchange) on single words |
Lock-free (CPU instructions) |
Mutex<T> |
Exclusive mutual exclusion over a protected value T |
Blocking (OS futex) |
RwLock<T> |
Concurrent multi-reader OR exclusive single-writer access to T |
Blocking (OS futex) |
Condvar |
Condition variable that puts a thread to sleep while releasing a MutexGuard until notified by another thread (.wait(), .notify_one(), .notify_all()) |
Blocking |
Barrier |
Rendezvous point where N threads wait until all N threads have arrived before any proceeds |
Blocking |
Once / OnceLock<T> / LazyLock<T> |
Guarantees one-time initialization across threads | Blocks only during first init; lock-free thereafter |
Coordinating Phase Transitions with std::sync::Barrier
A Barrier synchronizes a fixed group of $N$ threads at a checkpoint: calling barrier.wait() blocks each arriving thread until the $N$-th thread calls wait(), at which point all $N$ threads wake up simultaneously (and exactly one thread receives a BarrierWaitResult where .is_leader() is true):
use std::sync::Barrier;
use std::thread;
fn main() {
let worker_count = 3;
let barrier = Barrier::new(worker_count);
thread::scope(|s| {
for id in 0..worker_count {
let barrier_ref = &barrier;
s.spawn(move || {
// Phase 1: Every thread completes setup before ANY thread starts Phase 2
println!("worker {id}: finished phase 1 setup");
let wait_result = barrier_ref.wait();
if wait_result.is_leader() {
println!("--- leader (worker {id}) observed all threads ready ---");
}
println!("worker {id}: running phase 2");
});
}
});
}
worker 0: finished phase 1 setup
worker 1: finished phase 1 setup
worker 2: finished phase 1 setup
--- leader (worker 2) observed all threads ready ---
worker 2: running phase 2
worker 0: running phase 2
worker 1: running phase 2