| |

Rust 102 🦀 Memory Ordering Guarantees (SeqCst, Acquire, Release, Relaxed)

⏱️ Estimated read: 14 min | |

Every atomic operation in Rust carries an Ordering argument, and that argument is the entire reason atomics are difficult. The atomicity itself is simple: an operation that cannot be split by another thread. The ordering is where the reasoning lives. It determines which other memory operations the compiler and CPU are allowed to move around the atomic, and therefore which writes made by one thread become visible to another and in what order.

This chapter covers the five variants of std::sync::atomic::Ordering as they are defined in Rust’s standard library, which follows the C++20 memory model . It begins with the reordering problem that makes ordering necessary, then explains each variant in terms of the synchronization relationship it establishes. It addresses the compare_exchange failure-ordering restriction, the practical strategy for choosing an ordering, and the reason SeqCst is the correct starting point rather than the performance-optimized choice.


Key point: Ordering is about visibility, not just atomicity. A Relaxed operation is still atomic; it simply provides no guarantee that writes made by one thread are visible to another in any particular order. The ordering argument is how you tell the compiler which visibility guarantees your algorithm relies on.


Why memory ordering exists

The reordering problem. The compiler and the CPU both reorder instructions for performance. A store to a data variable that appears before a store to a flag in the source code may execute after it on the hardware. If another thread reads the flag and then reads the data, it may see the flag set but the data still at its old value. The atomicity of the flag store does not prevent this; the flag is written atomically, but the data write may not be visible yet. Ordering is the mechanism that prevents this reordering at the points where it matters.

The hardware variation. x86-64 provides strong ordering guarantees by default: loads are not reordered with loads, stores are not reordered with stores, and loads are not reordered with older stores. ARM and PowerPC provide weaker guarantees, and code that works on x86 by accident can fail on ARM because the compiler or CPU reordered something the programmer assumed was ordered . Explicit orderings make the synchronization requirements portable rather than hardware-dependent.

The cost of over-synchronization. SeqCst on every atomic operation is correct but may be slower on weakly-ordered platforms, where it requires additional fence instructions. Relaxed is cheaper because it allows the hardware to reorder freely. The trade-off is between the cost of synchronization and the complexity of reasoning about weaker guarantees. For most code, the performance difference is negligible and SeqCst is the right default .

The synchronization edge. The Acquire/Release pair is the core of Rust’s memory model. A Release store on one thread and an Acquire load of the same atomic on another thread establish a happens-before relationship: everything the first thread wrote before the Release store is visible to the second thread after the Acquire load . This is the mechanism for publishing data between threads without a mutex.


a. Relaxed: atomicity without synchronization

Relaxed is the weakest ordering. It guarantees only that the operation itself is atomic: no other thread can observe a partial result. It provides no happens-before relationship and no ordering constraints on surrounding memory operations. The compiler and CPU may reorder any other reads or writes across a Relaxed atomic .

Relaxed is appropriate for state that is independent of other data. A statistics counter incremented by many threads does not need to synchronize with anything else; each increment is atomic, and the final value is the sum of all increments regardless of the order in which they occurred. A unique ID generator using fetch_add is another example: the atomicity ensures no two threads get the same ID, and the order of allocation does not matter .

use std::sync::atomic::{AtomicUsize, Ordering};

// A statistics counter. The increments are atomic,
// but no other memory is synchronized by them.
static REQUESTS: AtomicUsize = AtomicUsize::new(0);

fn record_request() {
    // Relaxed is correct: the counter is independent state.
    // No other thread needs to see anything else because
    // of this increment.
    REQUESTS.fetch_add(1, Ordering::Relaxed);
}

Relaxed is not appropriate for publishing data. If a thread writes a value and then stores a flag with Relaxed, another thread that loads the flag with Relaxed has no guarantee that the value write is visible. The flag is atomic, but the data behind it is not synchronized .


b. Release and Acquire: the synchronization pair

Release and Acquire are designed to be used together. A Release store on one thread guarantees that all previous writes performed by that thread are visible to any thread that performs an Acquire load of the same atomic and observes the stored value . The pair establishes a happens-before edge: the writes before the Release store happen-before the reads after the Acquire load.

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

// Shared state: data is published behind a flag.
let data = Arc::new(std::sync::Mutex::new(0));
let ready = Arc::new(AtomicBool::new(false));

// Producer thread
let data_clone = Arc::clone(&data);
let ready_clone = Arc::clone(&ready);
let producer = thread::spawn(move || {
    *data_clone.lock().unwrap() = 42;
    // Release: the write to data is visible to any thread
    // that acquires the flag and sees true.
    ready_clone.store(true, Ordering::Release);
});

// Consumer thread
let data_clone = Arc::clone(&data);
let ready_clone = Arc::clone(&ready);
let consumer = thread::spawn(move || {
    // Acquire: wait for the flag. Once true is observed,
    // the write to data is guaranteed visible.
    while !ready_clone.load(Ordering::Acquire) {
        std::hint::spin_loop();
    }
    assert_eq!(*data_clone.lock().unwrap(), 42);
});

The Acquire load acts as a barrier: subsequent reads and writes cannot be moved before it. The Release store acts as a barrier: previous reads and writes cannot be moved after it. Together they ensure that the producer’s data write is ordered before the flag store, and the consumer’s data read is ordered after the flag load .

The pairing is strict: the Release and Acquire must be on the same atomic variable. A Release store to one variable and an Acquire load from a different variable do not synchronize, even if they are conceptually related .


c. AcqRel and SeqCst: the stronger guarantees

AcqRel combines Acquire and Release in a single read-modify-write operation. For loads, it behaves as Acquire; for stores, it behaves as Release. It is only applicable to operations that both read and write, such as compare_exchange or fetch_add .

The subtlety with compare_exchange is that the operation may fail, and a failed CAS performs no store. In that case, AcqRel provides only Acquire ordering. The standard library documentation states that AcqRel will never perform Relaxed accesses, but a failed AcqRel CAS is effectively an Acquire load .

use std::sync::atomic::{AtomicUsize, Ordering};

// A lock-free increment using AcqRel: the RMW both reads and writes.
fn increment(counter: &AtomicUsize) {
    let mut current = counter.load(Ordering::Relaxed);
    loop {
        match counter.compare_exchange_weak(
            current,
            current + 1,
            Ordering::AcqRel,   // success: Acquire + Release
            Ordering::Relaxed,  // failure: Relaxed is sufficient here
        ) {
            Ok(_) => return,
            Err(actual) => current = actual,
        }
    }
}

SeqCst is AcqRel plus a total global order. Every SeqCst operation appears to all threads to occur in the same order. This is stronger than the per-variable synchronization of Acquire/Release: it means that if thread A performs a SeqCst store and thread B performs a SeqCst load, and thread C performs a different SeqCst operation, all threads agree on the relative order of these operations .

SeqCst is the easiest ordering to reason about because it matches the intuitive model of a single sequential execution of all atomic operations. The Rust standard library documentation and practical guides recommend starting with SeqCst and weakening only when there is a measured reason and a written proof .

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

// A stop flag that multiple threads observe.
// SeqCst ensures all threads see the same order of operations.
static STOP: AtomicBool = AtomicBool::new(false);

fn worker() {
    while !STOP.load(Ordering::SeqCst) {
        // do work
    }
}

fn main() {
    // ... spawn workers ...
    STOP.store(true, Ordering::SeqCst);
}

Complete Example Session

// ============================================
// PART 1: THE REORDERING PROBLEM
// ============================================
// A store to data followed by a store to a flag.
// Without ordering, the flag may become visible first.

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};

static DATA: AtomicUsize = AtomicUsize::new(0);
static FLAG: AtomicBool = AtomicBool::new(false);

// ============================================
// PART 2: RELAXED IS NOT ENOUGH FOR PUBLICATION
// ============================================
// This producer/consumer pair is incorrect.
// The consumer may see the flag but not the data.

fn producer_relaxed() {
    DATA.store(42, Ordering::Relaxed);  // may be reordered after flag
    FLAG.store(true, Ordering::Relaxed);
}

fn consumer_relaxed() -> Option<usize> {
    if FLAG.load(Ordering::Relaxed) {
        Some(DATA.load(Ordering::Relaxed)) // may see 0, not 42
    } else {
        None
    }
}

// ============================================
// PART 3: THE RELEASE/ACQUIRE PAIR
// ============================================
// Release orders the data write before the flag store.
// Acquire orders the data read after the flag load.

fn producer_release() {
    DATA.store(42, Ordering::Relaxed);  // plain store is fine
    FLAG.store(true, Ordering::Release); // barrier: data is visible
}

fn consumer_acquire() -> Option<usize> {
    if FLAG.load(Ordering::Acquire) {    // barrier: data is visible
        Some(DATA.load(Ordering::Relaxed))
    } else {
        None
    }
}

// ============================================
// PART 4: RELAXED FOR INDEPENDENT STATE
// ============================================
// A counter that synchronizes nothing is correct with Relaxed.

static COUNT: AtomicUsize = AtomicUsize::new(0);

fn increment() {
    // Atomic, no synchronization needed.
    COUNT.fetch_add(1, Ordering::Relaxed);
}

// ============================================
// PART 5: ACQREL FOR READ-MODIFY-WRITE
// ============================================
// A CAS loop that both reads and writes uses AcqRel.

fn cas_increment(counter: &AtomicUsize) {
    let mut current = counter.load(Ordering::Relaxed);
    loop {
        match counter.compare_exchange_weak(
            current,
            current + 1,
            Ordering::AcqRel,   // success: Acquire + Release
            Ordering::Relaxed,  // failure: Relaxed is sufficient
        ) {
            Ok(_) => return,
            Err(actual) => current = actual,
        }
    }
}

// ============================================
// PART 6: THE FAILURE ORDERING RESTRICTION
// ============================================
// compare_exchange failure cannot be Release or AcqRel.
// The operation may not perform a store, so Release is meaningless.

// Valid failure orderings: Relaxed, Acquire, SeqCst.
// Invalid: Release, AcqRel.

// ============================================
// PART 7: SEQ_CST FOR GLOBAL ORDER
// ============================================
// When multiple atomics coordinate, SeqCst gives a total order.
// Start here when the reasoning is not obvious.

fn coordinated() {
    // All SeqCst operations appear in the same order to all threads.
    DATA.store(42, Ordering::SeqCst);
    FLAG.store(true, Ordering::SeqCst);
}

// ============================================
// PART 8: THE PRACTICAL STRATEGY
// ============================================
// 1. Start with SeqCst for correctness.
// 2. Add tests and benchmarks.
// 3. Weaken one edge at a time, with a written proof.
// 4. Keep a comment explaining the synchronization edge.

// ============================================
// PART 9: THE HARDWARE VARIATION
// ============================================
// x86 provides strong ordering by default.
// ARM and PowerPC are weaker; Relaxed is cheaper but
// the synchronization edges must be explicit.

// Code that "works on x86" may fail on ARM if it relies
// on accidental ordering rather than explicit orderings.

// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// Ordering is the contract between the programmer and
// the compiler/CPU about what may be reordered.

fn main() {
    producer_release();
    assert_eq!(consumer_acquire(), Some(42));

    increment();
    cas_increment(&COUNT);
}

The ten parts build from the reordering problem to the correct Release/Acquire pair, with the Relaxed counter and the AcqRel CAS loop as contrast. The failure-ordering restriction and the SeqCst total order complete the picture. The practical strategy is the one the standard library documentation and the Rust community consistently recommend: start with SeqCst, weaken only with evidence .


Quick Reference

Ordering Variants

OrderingApplies ToGuaranteeTypical Use
RelaxedLoad, Store, RMWAtomicity onlyCounters, statistics
ReleaseStorePrior writes visible to AcquireProducer flag store
AcquireLoadSubsequent reads see prior writesConsumer flag load
AcqRelRMWBoth Acquire and ReleaseCAS loops
SeqCstAllTotal global orderDefault safe choice

compare_exchange Orderings

SuccessFailureValid?
SeqCstSeqCstYes
AcqRelAcquireYes
AcqRelRelaxedYes
ReleaseRelaxedNo (failure cannot be Release)
RelaxedAcqRelNo (failure cannot be AcqRel)

Intent to Ordering

IntentProducerConsumer
Independent counterRelaxedRelaxed
Publish data via flagRelease storeAcquire load
Read-modify-writeAcqRel—
Unsure / complexSeqCstSeqCst

Hardware Notes

PlatformDefault StrengthRelaxed Cost
x86-64StrongMinimal
ARMWeakCheaper, but explicit edges required
PowerPCWeakExplicit fences needed

Best Practices

✅ Do This:

// Start with SeqCst when reasoning is unclear.
FLAG.store(true, Ordering::SeqCst);                                  // ✅

// Pair Release store with Acquire load.
FLAG.store(true, Ordering::Release);                                 // ✅
while !FLAG.load(Ordering::Acquire) { spin_loop(); }                 // ✅

// Use Relaxed for independent counters.
COUNT.fetch_add(1, Ordering::Relaxed);                               // ✅

// Use AcqRel for CAS loops that synchronize.
counter.compare_exchange_weak(a, b, AcqRel, Relaxed);                // ✅

// Document the synchronization edge in a comment.
// Release: DATA is visible after FLAG is true.                     // ✅

❌ Don’t Do This:

// Don't use Relaxed to publish data.
DATA.store(42, Relaxed); FLAG.store(true, Relaxed);                  // ❌

// Don't pair Release with Relaxed.
FLAG.store(true, Release); if FLAG.load(Relaxed) { ... }             // ❌

// Don't use Release or AcqRel as compare_exchange failure.
compare_exchange(a, b, SeqCst, Release);                             // ❌

// Don't assume x86 ordering makes Relaxed safe for publication.
// It is an accidental guarantee, not a portable one.               // ❌

// Don't weaken ordering without a benchmark and proof.
// SeqCst is the correct default.                                   // ❌

Common Pitfalls

PitfallWhy It HappensFix
Data not visible after flagRelaxed used for publicationPair Release store with Acquire load
Works on x86, fails on ARMAccidental hardware orderingUse explicit Acquire/Release
compare_exchange compile errorRelease used as failure orderingUse Relaxed, Acquire, or SeqCst for failure
Unnecessary SeqCst overheadDefault used everywhereWeaken to Acquire/Release with proof
Lost synchronizationRelease and Acquire on different variablesPair on the same atomic
Relaxed counter expected to synchronizeMisunderstanding of RelaxedUse AcqRel or SeqCst if other data is involved

Real-World Examples

1. Independent Counter

COUNT.fetch_add(1, Ordering::Relaxed);

2. Publish Data

DATA.store(42, Ordering::Relaxed);
FLAG.store(true, Ordering::Release);

3. Consume Data

if FLAG.load(Ordering::Acquire) {
    assert_eq!(DATA.load(Ordering::Relaxed), 42);
}

4. CAS Loop

counter.compare_exchange_weak(a, b, Ordering::AcqRel, Ordering::Relaxed);

5. Stop Flag

STOP.store(true, Ordering::SeqCst);
while !STOP.load(Ordering::SeqCst) {}

6. Failed CAS Ordering

compare_exchange(a, b, Ordering::SeqCst, Ordering::Acquire);

7. Relaxed Load

let value = DATA.load(Ordering::Relaxed);

8. Acquire-Only Load

let ready = FLAG.load(Ordering::Acquire);

9. Release-Only Store

FLAG.store(true, Ordering::Release);

10. SeqCst Default

ATOMIC.store(1, Ordering::SeqCst);

Visual

The Reordering Problem

┌─────────────────────────────────────────────────────────┐
│  SOURCE CODE                                            │
│                                                         │
│  DATA.store(42, Relaxed);                               │
│  FLAG.store(true, Relaxed);                             │
│                                                         │
│  WITHOUT ORDERING:                                      │
│                                                         │
│  CPU may execute:                                       │
│  FLAG.store(true, Relaxed);   ← moved before            │
│  DATA.store(42, Relaxed);                               │
│                                                         │
│  Consumer sees FLAG = true, then reads DATA = 0.        │
│  The data was not published.                            │
└─────────────────────────────────────────────────────────┘

The Release/Acquire Pair

┌─────────────────────────────────────────────────────────┐
│  PRODUCER                                               │
│                                                         │
│  DATA.store(42, Relaxed);                               │
│  FLAG.store(true, Release);                             │
│         │                                               │
│         │  Release barrier: DATA write cannot move      │
│         │  after FLAG store.                            │
│         ▼                                               │
│  FLAG = true is visible.                                │
│  DATA = 42 is visible.                                  │
│                                                         │
│  ─────────────────────────────────────────────────────  │
│                                                         │
│  CONSUMER                                               │
│                                                         │
│  if FLAG.load(Acquire) {                                │
│         │                                               │
│         │  Acquire barrier: DATA read cannot move       │
│         │  before FLAG load.                            │
│         ▼                                               │
│      DATA.load(Relaxed) → 42                            │
│  }                                                      │
└─────────────────────────────────────────────────────────┘

The Ordering Spectrum

┌─────────────────────────────────────────────────────────┐
│  Relaxed     Atomic only. No synchronization.           │
│              Counters, statistics.                      │
│                                                         │
│  Acquire     Load barrier. Subsequent ops cannot        │
│              move before. Consumer side.                │
│                                                         │
│  Release     Store barrier. Prior ops cannot            │
│              move after. Producer side.                 │
│                                                         │
│  AcqRel      Both. Read-modify-write. CAS loops.        │
│                                                         │
│  SeqCst      Total global order. Default safe choice.   │
└─────────────────────────────────────────────────────────┘

compare_exchange Failure Restriction

┌─────────────────────────────────────────────────────────┐
│  compare_exchange(current, new, success, failure)       │
│                                                         │
│  SUCCESS orderings:                                     │
│    Relaxed, Acquire, Release, AcqRel, SeqCst            │
│                                                         │
│  FAILURE orderings:                                     │
│    Relaxed, Acquire, SeqCst                             │
│    (cannot be Release or AcqRel)                        │
│                                                         │
│  Why: a failed CAS performs no store.                   │
│  Release ordering requires a store to order against.    │
│  AcqRel requires a store for its Release half.          │
└─────────────────────────────────────────────────────────┘

Summary

ItemValue
Modulestd::sync::atomic::Ordering
WeakestRelaxed (atomicity only)
ProducerRelease store
ConsumerAcquire load
Read-modify-writeAcqRel
StrongestSeqCst (total global order)
Failure restrictionCannot be Release or AcqRel
Default recommendationSeqCst
Hardware variationx86 strong, ARM weak
Synchronization unitSame atomic variable

Key takeaways:

  • Ordering controls reordering, not atomicity. A Relaxed operation is atomic. It simply allows the compiler and CPU to reorder other memory operations around it .
  • Release and Acquire are a pair on the same atomic. The Release store publishes prior writes; the Acquire load makes them visible. Using them on different variables does not synchronize .
  • Relaxed is for independent state. A statistics counter does not need to synchronize with anything. A flag that publishes data does .
  • SeqCst is the safe default. Start with it. Weaken only when there is a benchmark and a written proof that the weaker ordering is correct .
  • The compare_exchange failure ordering is restricted. Release and AcqRel are invalid as failure orderings because a failed CAS performs no store .
  • Hardware ordering is not portable. Code that works on x86 with Relaxed may fail on ARM because x86 provides stronger ordering by accident, not by contract .
  • The synchronization edge is the reasoning unit. Ask “which write must become visible to which read?” and choose the ordering that establishes that edge. Do not sprinkle SeqCst everywhere without understanding why .
  • Fences are rarely needed. Prefer encoding ordering directly on the atomic operations. Use fence only when multiple atomics must be coordinated in a way that per-operation orderings cannot express .

Remember: Memory ordering is the contract between your algorithm and the hardware. The atomic operation guarantees that no thread sees a torn value. The ordering guarantees which other values that thread sees, and in what order. Relaxed says “I only care about this value.” Release says “everything before this is ready.” Acquire says “everything after this can trust what came before.” SeqCst says “everyone agrees on the order.” Start with SeqCst, understand the edge you need, and weaken only when you can explain the edge in a sentence and a benchmark shows it matters.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!