Synchronization Primitives

Memory Ordering

A CPU's memory model and Rust's C11-derived memory ordering model define which values an atomic read on one thread can observe when multiple threads read and write shared memory across multi-core caches and store buffers.

Hardware Reality: Why Memory Operations Appear Out of Order

Modern CPUs do not read and write main RAM directly on every instruction. Each CPU core has a private store buffer and a hierarchy of L1/L2 caches:

  • On x86_64 (Total Store Order — TSO): Stores are never reordered with other stores, and loads are never reordered with other loads, but a load can be reordered before an earlier store to a different address (because the store sits in the core's store buffer while the load hits the L1 cache immediately!).
  • On AArch64 (ARM64) and RISC-V (Weakly Ordered): Both loads and stores to different addresses can be reordered freely by the hardware unless explicit acquire/release instructions (ldar, stlr) or memory barrier fences (dmb) are emitted!
CPU Core Store Buffer Reordering (Why `Relaxed` Can Observe Stale Data):

  Core 0 (Writer)                        Core 1 (Reader)
  ---------------                        ---------------
  DATA = 42;     --> [ Store Buffer ]    while !READY.load(Relaxed) {}
  READY = true;  --> (Flushed first!) -> Sees READY == true!
                                         Reads DATA from L1 cache -> Sees 0 (Stale)!

How Release and Acquire Form a Memory Barrier Synchronizes-With Edge

When Thread A performs a store with Ordering::Release (or Ordering::AcqRel / Ordering::SeqCst) to an atomic variable M, and Thread B performs a load with Ordering::Acquire (or Ordering::AcqRel / Ordering::SeqCst) from the same variable M that observes the value written by Thread A:

  1. Synchronizes-With: Thread A's Release store synchronizes-with Thread B's Acquire load.
  2. Happens-Before: Every memory write (whether atomic, Relaxed, or non-atomic inside an UnsafeCell) executed by Thread A before the Release store is guaranteed to be visible to Thread B after the Acquire load!
Thread A (Producer)                        Thread B (Consumer)
-------------------                        -------------------
1. non_atomic_buffer = "hello";
2. metric.store(10, Relaxed);
======== [ Release Barrier ] ========>
3. flag.store(true, Release);   ---sync--> 4. while !flag.load(Acquire) {}
                                           ======== [ Acquire Barrier ] ========
                                           5. Read `non_atomic_buffer` ("hello")
                                           6. Read `metric.load(Relaxed)` (10)

Standalone Atomic Fences (std::sync::atomic::fence)

std::sync::atomic::fence(Ordering) establishes a memory ordering barrier between atomic operations without attaching the ordering to a single load or store—useful when separating a relaxed atomic decrement from a cold cleanup path (as used inside Arc::drop!):

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering, fence};
use std::thread;

struct OneShotChannel<T> {
    message: UnsafeCell<Option<T>>,
    ready: AtomicBool,
}

// SAFETY: `ready` synchronizes the write by the sender with the read by the receiver.
unsafe impl<T: Send> Sync for OneShotChannel<T> {}

impl<T> OneShotChannel<T> {
    const fn new() -> Self {
        Self {
            message: UnsafeCell::new(None),
            ready: AtomicBool::new(false),
        }
    }
}

fn main() {
    let channel = OneShotChannel::new();

    thread::scope(|s| {
        s.spawn(|| {
            // SAFETY: Written before `ready` is set to `true`.
            unsafe {
                *channel.message.get() = Some(String::from("synchronized via fence"));
            }
            // A `Release` fence followed by a `Relaxed` store establishes a
            // release sequence with any thread that acquires `ready == true`:
            fence(Ordering::Release);
            channel.ready.store(true, Ordering::Relaxed);
        });

        s.spawn(|| {
            while !channel.ready.load(Ordering::Relaxed) {
                std::hint::spin_loop();
            }
            // Pair with an `Acquire` fence after observing `ready == true`:
            fence(Ordering::Acquire);
            // SAFETY: The Release/Acquire fence pair guarantees the write to `message`
            // happened-before this read!
            let msg = unsafe { (*channel.message.get()).take().unwrap() };
            println!("received: {:?}", msg);
        });
    });
}
received: "synchronized via fence"

Hardware Instructions Emitted per Ordering

Ordering x86_64 Instruction AArch64 (ARM64) Instruction
load(Relaxed) mov ldr
load(Acquire) mov (TSO hardware provides acquire for free!) ldar (Load-Acquire)
store(Relaxed) mov str
store(Release) mov (TSO hardware provides release for free!) stlr (Store-Release)
store(SeqCst) xchg (or mov + mfence) stlr (paired with ldar for SeqCst)