Frontend Labs
⋯

61. Memory ordering: Relaxed, Acquire/Release, SeqCst, and a spinlock

CPUs and compilers reorder memory operations. An atomic ordering states which reorderings are not allowed. Rust uses the C++20/C11 model, so the C guide §38 applies one-to-one.

Ordering Guarantees Use for
Relaxed atomicity only; no ordering with other memory counters, statistics
Release (store) / Acquire (load) everything written before the Release store is visible to the thread whose Acquire load reads that store publishing data, locks
AcqRel both, for read-modify-write operations compare_exchange in lock-free structures
SeqCst Acquire/Release plus one global order of all SeqCst operations when you cannot prove a weaker ordering is enough

61.1 Publishing data with Release/Acquire

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;

static DATA: AtomicU64 = AtomicU64::new(0);
static READY: AtomicBool = AtomicBool::new(false);

fn main() {
    let producer = thread::spawn(|| {
        DATA.store(42, Ordering::Relaxed);          // (1) write the payload
        READY.store(true, Ordering::Release);       // (2) publish: (1) cannot move after this
    });
    let consumer = thread::spawn(|| {
        while !READY.load(Ordering::Acquire) {      // (3) once this sees true...
            std::hint::spin_loop();
        }
        DATA.load(Ordering::Relaxed)                // (4) ...this is guaranteed to see 42
    });
    producer.join().unwrap();
    println!("consumer saw {}", consumer.join().unwrap());
}

With Relaxed on both READY operations, a weakly ordered CPU such as an ARM server or an Apple Silicon Mac may legally let the consumer see READY == true and DATA == 0. On x86 the hardware hides most of these reorderings, so bugs like this can pass every test on a developer laptop and then fail on Graviton. Test lock-free code with loom, which explores the possible interleavings and orderings systematically.

61.2 A spinlock with the right orderings

use std::cell::UnsafeCell;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SpinLock<T> { locked: AtomicBool, value: UnsafeCell<T> }

// SAFETY: the lock gives one thread at a time access to `value`, so sharing &SpinLock across
// threads is sound whenever T itself may be sent between threads.
unsafe impl<T: Send> Sync for SpinLock<T> {}

pub struct Guard<'a, T> { lock: &'a SpinLock<T> }

impl<T> SpinLock<T> {
    pub const fn new(value: T) -> Self { Self { locked: AtomicBool::new(false), value: UnsafeCell::new(value) } }

    pub fn lock(&self) -> Guard<'_, T> {
        // Acquire on success: we must see every write made by the previous holder before its Release.
        while self.locked.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
            while self.locked.load(Ordering::Relaxed) { std::hint::spin_loop(); } // spin on reads, not on CAS
        }
        Guard { lock: self }
    }
}

impl<T> Deref for Guard<'_, T> {
    type Target = T;
    // SAFETY: holding the Guard means we hold the lock: no other reference to `value` exists.
    fn deref(&self) -> &T { unsafe { &*self.lock.value.get() } }
}
impl<T> DerefMut for Guard<'_, T> {
    fn deref_mut(&mut self) -> &mut T { unsafe { &mut *self.lock.value.get() } }
}
impl<T> Drop for Guard<'_, T> {
    fn drop(&mut self) { self.lock.locked.store(false, Ordering::Release); } // publish our writes
}

fn main() {
    static COUNTER: SpinLock<u64> = SpinLock::new(0);
    std::thread::scope(|s| {
        for _ in 0..8 {
            s.spawn(|| for _ in 0..100_000 { *COUNTER.lock() += 1; });
        }
    });
    println!("{}", *COUNTER.lock()); // 800000, every time
}

The guard is what makes this a Rust API rather than a C API. You cannot reach the data without locking, and you cannot forget to unlock. Do not use spinlocks in user space in practice. A preempted lock holder makes every waiter burn a full time slice. std::sync::Mutex spins briefly and then sleeps on a futex, which is what you want (OS Ch 18). Spinlocks belong in kernels and interrupt handlers (C §52).


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36