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.

05. Concurrency, Atomics & Asynchronous Architecture7 min read

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.

Synchronization InvariantMemory Ordering
Acq-Rel

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#

Architecture Diagram
Rendering diagram...

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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use 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 OrderingHardware Synchronization GuaranteeOverhead
RelaxedGuarantees atomicity of the single variable only. Zero cross-variable ordering fences.Lowest (Raw CPU operation).
AcquireUsed on loads. Subsequent reads cannot be reordered before this operation.Medium (Invalidates read buffers).
ReleaseUsed on stores. Prior writes cannot be reordered after this operation.Medium (Flushes write buffers).
AcqRelCombines Acquire and Release for Read-Modify-Write instructions (fetch_add).High (Two-way memory barrier).
SeqCstSequentially 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:

Thread A:Store(X,Release)  ⟹  Thread B:Load(X,Acquire)  ⟹  All prior writes by A are visible to B\text{Thread } A: \text{Store}(X, \text{Release}) \implies \text{Thread } B: \text{Load}(X, \text{Acquire}) \implies \text{All prior writes by } A \text{ are visible to } B
Architecture Diagram
Rendering diagram...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use 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:

Concept check

What happens when a thread panics while holding an active MutexGuard in Rust?

Concept check

How does an Acquire-Release pair guarantee memory visibility between two threads?