Low-Level Systems Programming
CPU Memory Ordering
Modern compilers and multi-core CPUs aggressively reorder memory reads and writes (via store buffers, invalidation queues, and out-of-order execution pipelines) whenever those accesses target different memory addresses. std::sync::atomic::Ordering specifies the exact happens-before memory visibility constraints around an atomic operation so that writes to non-atomic data on one CPU core become visible to another CPU core in a predictable order.
Why Memory Reordering Happens Without Acquire/Release Synchronization:
Producer Thread (Core 0): Consumer Thread (Core 1):
1. PAYLOAD = 42; (Buffered in Store Buf) 1. Sees READY == true!
2. READY = true; (Flushed first!) 2. Reads PAYLOAD -> Sees stale 0!
Comparing the Five Ordering Variants
Ordering Variant |
Valid On | Synchronization & Hardware Guarantee |
|---|---|---|
Relaxed |
Load, Store, RMW | Guarantees atomicity of only this single variable; imposes zero ordering constraints on surrounding memory reads or writes (mov on x86/ARM). Use for standalone counters or IDs. |
Release |
Store | Publishing barrier: Every memory read and write before this store(Release) in program order is guaranteed to complete and be visible before the store itself becomes visible (stlr on ARM64). |
Acquire |
Load | Subscribing barrier: Every memory read and write after this load(Acquire) in program order is guaranteed to execute after observing the value written by a matching Release store (ldar on ARM64). |
AcqRel |
RMW (fetch_*, swap, CAS) |
Combines Acquire on the read half and Release on the write half of a read-modify-write operation. |
SeqCst |
Load, Store, RMW | All Acquire/Release/AcqRel guarantees plus a single global total order of all SeqCst operations across all threads (mfence / lock on x86_64). |
Release-Acquire Synchronization Edge ("Happens-Before"):
Producer Thread Consumer Thread
--------------- ---------------
1. Write `data = "ready payload"`
2. Write `meta = 123`
3. `flag.store(true, Ordering::Release)` ===== Synchronizes-With =====> 4. `while !flag.load(Ordering::Acquire) {}`
---------------------------------------- ----------------------------------------
Everything above line 3 is guaranteed 5. Read `data` & `meta` (Guaranteed to see
to happen-before line 4 observes `true`! `"ready payload"` and `123`!)
Implementing a Lock-Free Single-Value Handoff with Release and Acquire
use std::cell::UnsafeCell;
use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
pub struct OneShotSlot<T> {
ready: AtomicBool,
data: UnsafeCell<MaybeUninit<T>>,
}
// SAFETY: Access to `data` is synchronized by `ready` using Release/Acquire ordering.
unsafe impl<T: Send> Sync for OneShotSlot<T> {}
impl<T> OneShotSlot<T> {
pub const fn new() -> Self {
Self {
ready: AtomicBool::new(false),
data: UnsafeCell::new(MaybeUninit::uninit()),
}
}
pub fn publish(&self, value: T) {
// 1. Write payload into `UnsafeCell` first
unsafe {
(*self.data.get()).write(value);
}
// 2. `Ordering::Release` guarantees the write to `self.data` above
// is visible in memory BEFORE `ready` becomes `true`!
self.ready.store(true, Ordering::Release);
}
pub fn try_read(&self) -> Option<&T> {
// 3. `Ordering::Acquire` guarantees that if we observe `true`, all writes
// performed before the `Release` store are visible to our subsequent read!
if self.ready.load(Ordering::Acquire) {
Some(unsafe { (*self.data.get()).assume_init_ref() })
} else {
None
}
}
}
fn main() {
let slot = OneShotSlot::new();
thread::scope(|s| {
s.spawn(|| {
slot.publish(String::from("synchronized via Release/Acquire"));
});
while slot.try_read().is_none() {
std::hint::spin_loop();
}
});
println!("Slot value: {:?}", slot.try_read().unwrap());
}
Slot value: "synchronized via Release/Acquire"
Hardware Fences (fence) vs Compiler Fences (compiler_fence)
std::sync::atomic::fence(Ordering): Emits a CPU hardware memory barrier instruction (dmb ishon ARM64,mfenceon x86_64 forSeqCst) to order surrounding relaxed atomic operations across CPU cores.std::sync::atomic::compiler_fence(Ordering): Emits zero CPU instructions, preventing only the compiler from reordering loads and stores across the fence on the same CPU core (used when synchronizing a thread with a signal handler or interrupt service routine running on the same core).