Synchronization Primitives: Mutex, RwLock, Atomics & Memory Ordering
Master Mutex locks, lock poisoning recovery, RwLock reader-writer patterns, hardware atomics, and Acquire-Release memory ordering fences.
Synchronization primitives coordinate memory access across concurrent execution threads. Rust categorizes synchronization into operating-system locks (Mutex<T>, RwLock<T>) and hardware atomic instructions (AtomicUsize, AtomicBool).
Choosing the correct primitive and memory ordering is essential for maximizing multi-core throughput while avoiding deadlocks.
A Release store in Thread A synchronizes with an Acquire load in Thread B, making all prior writes visible.
1. Operating System Locks: Mutex vs RwLock#
Mutex Lock Poisoning#
If a thread panics while holding a MutexGuard, Rust flags the mutex as poisoned. Subsequent attempts to call .lock() return an Err(PoisonError):
12345678910111213141516171819202122232425use std::sync::{Arc, Mutex}; use std::thread; fn main() { let data = Arc::new(Mutex::new(10)); let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { let mut guard = data_clone.lock().unwrap(); *guard = 20; panic!("Thread panicked while holding lock!"); }); let _ = handle.join(); // Child panicked // Parent acquires lock: returns Err(PoisonError) match data.lock() { Ok(guard) => println!("Value: {}", *guard), Err(poisoned) => { eprintln!("Warning: Mutex is poisoned! Recovering data..."); let recovered_guard = poisoned.into_inner(); println!("Recovered value: {}", *recovered_guard); // 20 } } }
2. Hardware Atomics & Memory Ordering#
For scalar numbers and flags, locks introduce heavy operating system context switching overhead. Atomics execute directly in hardware using CPU bus synchronization instructions.
Every atomic operation takes an explicit std::sync::atomic::Ordering specifying how the CPU and compiler are permitted to reorder instructions:
| Memory Ordering | Hardware Synchronization Guarantee | Overhead |
|---|---|---|
Relaxed | Guarantees atomicity of the single variable only. Zero cross-variable ordering fences. | Lowest (Raw CPU operation). |
Acquire | Used on loads. Subsequent reads cannot be reordered before this operation. | Medium (Invalidates read buffers). |
Release | Used on stores. Prior writes cannot be reordered after this operation. | Medium (Flushes write buffers). |
AcqRel | Combines Acquire and Release for Read-Modify-Write instructions (fetch_add). | High (Two-way memory barrier). |
SeqCst | Sequentially Consistent: globally consistent total ordering across all CPU cores. | Highest (Full hardware fence). |
3. The Acquire-Release Synchronization Invariant#
The most critical synchronization pattern in high-performance computing is the Acquire-Release fence:
1234567891011121314151617181920212223242526272829303132use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::thread; struct SharedChannel { data: AtomicU32, ready: AtomicBool, } fn main() { let channel = Arc::new(SharedChannel { data: AtomicU32::new(0), ready: AtomicBool::new(false), }); let chan_clone = Arc::clone(&channel); thread::spawn(move || { // Step 1: Write payload with Relaxed chan_clone.data.store(42, Ordering::Relaxed); // Step 2: Signal readiness with Release fence! chan_clone.ready.store(true, Ordering::Release); }); // Consumer polls flag while !channel.ready.load(Ordering::Acquire) { std::hint::spin_loop(); } // Because ready was loaded with Acquire, data is guaranteed to be 42! let payload = channel.data.load(Ordering::Relaxed); println!("Payload received: {}", payload); }
4. Concept Verification#
Test your understanding of Mutex locks and atomic memory ordering:
What happens when a thread panics while holding an active MutexGuard in Rust?
How does an Acquire-Release pair guarantee memory visibility between two threads?