Rust 102 🦀 Memory Ordering Guarantees (SeqCst, Acquire, Release, Relaxed)
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
| Ordering | Applies To | Guarantee | Typical Use |
|---|---|---|---|
Relaxed | Load, Store, RMW | Atomicity only | Counters, statistics |
Release | Store | Prior writes visible to Acquire | Producer flag store |
Acquire | Load | Subsequent reads see prior writes | Consumer flag load |
AcqRel | RMW | Both Acquire and Release | CAS loops |
SeqCst | All | Total global order | Default safe choice |
compare_exchange Orderings
| Success | Failure | Valid? |
|---|---|---|
SeqCst | SeqCst | Yes |
AcqRel | Acquire | Yes |
AcqRel | Relaxed | Yes |
Release | Relaxed | No (failure cannot be Release) |
Relaxed | AcqRel | No (failure cannot be AcqRel) |
Intent to Ordering
| Intent | Producer | Consumer |
|---|---|---|
| Independent counter | Relaxed | Relaxed |
| Publish data via flag | Release store | Acquire load |
| Read-modify-write | AcqRel | — |
| Unsure / complex | SeqCst | SeqCst |
Hardware Notes
| Platform | Default Strength | Relaxed Cost |
|---|---|---|
| x86-64 | Strong | Minimal |
| ARM | Weak | Cheaper, but explicit edges required |
| PowerPC | Weak | Explicit 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Data not visible after flag | Relaxed used for publication | Pair Release store with Acquire load |
| Works on x86, fails on ARM | Accidental hardware ordering | Use explicit Acquire/Release |
| compare_exchange compile error | Release used as failure ordering | Use Relaxed, Acquire, or SeqCst for failure |
Unnecessary SeqCst overhead | Default used everywhere | Weaken to Acquire/Release with proof |
| Lost synchronization | Release and Acquire on different variables | Pair on the same atomic |
| Relaxed counter expected to synchronize | Misunderstanding of Relaxed | Use 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
| Item | Value |
|---|---|
| Module | std::sync::atomic::Ordering |
| Weakest | Relaxed (atomicity only) |
| Producer | Release store |
| Consumer | Acquire load |
| Read-modify-write | AcqRel |
| Strongest | SeqCst (total global order) |
| Failure restriction | Cannot be Release or AcqRel |
| Default recommendation | SeqCst |
| Hardware variation | x86 strong, ARM weak |
| Synchronization unit | Same atomic variable |
Key takeaways:
- Ordering controls reordering, not atomicity. A
Relaxedoperation is atomic. It simply allows the compiler and CPU to reorder other memory operations around it . ReleaseandAcquireare a pair on the same atomic. TheReleasestore publishes prior writes; theAcquireload makes them visible. Using them on different variables does not synchronize .Relaxedis for independent state. A statistics counter does not need to synchronize with anything. A flag that publishes data does .SeqCstis 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_exchangefailure ordering is restricted.ReleaseandAcqRelare invalid as failure orderings because a failed CAS performs no store . - Hardware ordering is not portable. Code that works on x86 with
Relaxedmay 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
SeqCsteverywhere without understanding why . - Fences are rarely needed. Prefer encoding ordering directly on the atomic operations. Use
fenceonly 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.
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!