An atomic operation protects one access from being torn or racing with other atomic accesses to the same location. Its memory ordering also determines which relationships it establishes with other operations.
Start with a concrete question: what must another thread be able to observe before it can safely continue? If the answer involves several operations, atomicity alone may not be enough.
Start with a synchronization primitive
A mutex, channel or thread join often supplies the coordination you need.
For example, a successful join() establishes that the joined thread's
operations happen before operations following the join. Locking a mutex
coordinates access with previous holders of that same mutex.
These guarantees let you reason about data access without choosing atomic orderings yourself. A condition variable is used with a mutex and a condition checked in a loop; notification alone is not a replacement for protecting the shared state, and waiting can wake spuriously.
Use atomics directly when their operations fit the problem, such as a counter, or when implementing a carefully designed synchronization protocol.
What each ordering adds
Rust has five orderings. The operation determines which ones are valid.
| Ordering | Role |
|---|---|
Relaxed |
Atomic access with a modification order for that atomic location, but no synchronization of other memory by itself. |
Release |
Publishes preceding operations through a store or the write part of a read-modify-write operation. |
Acquire |
Receives that publication through a load or the read part of a read-modify-write operation, when it observes the corresponding release. |
AcqRel |
Combines acquire and release for a read-modify-write operation. |
SeqCst |
Provides acquire for loads, release for stores, both for read-modify-write operations, plus a shared total order of sequentially consistent operations. |
A per-location modification order does not mean every load reads the latest value by wall-clock time. Memory ordering supplies no deadline for visibility or scheduling. It also does not mean “flush the cache,” and disabling compiler optimization does not remove the need for synchronization.
Example: a counter with no publication role
This counter records how many increments the workers performed. It does not signal that some separate piece of data is ready:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let count = AtomicUsize::new(0);
thread::scope(|scope| {
for _ in 0..4 {
let count = &count;
scope.spawn(move || {
for _ in 0..100 {
count.fetch_add(1, Ordering::Relaxed);
}
});
}
}); // All scoped threads have joined.
assert_eq!(count.load(Ordering::Relaxed), 400);
}
fetch_add performs an indivisible update, so concurrent increments are not
lost. A separate load followed by a store would not provide that guarantee,
even if both used SeqCst. The scope joins the workers before the final read.
Relaxed operations are enough here because no increment has to publish other
data to a worker or observer.
Example: publishing a value through a ready flag
A release store can synchronize with an acquire load that reads from that
store, or from its release sequence. A release sequence can continue through
atomic read-modify-write operations on the same location. In the simple case
below, there is only one write of true, so the connection is direct.
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
fn main() {
let payload = AtomicUsize::new(0);
let ready = AtomicBool::new(false);
thread::scope(|scope| {
scope.spawn(|| {
payload.store(42, Ordering::Relaxed);
ready.store(true, Ordering::Release);
});
scope.spawn(|| {
while !ready.load(Ordering::Acquire) {
thread::yield_now();
}
assert_eq!(payload.load(Ordering::Relaxed), 42);
});
});
}
The payload write precedes the release store. The successful acquire load
observes that store, and the payload read follows the acquire. Together these
relationships establish happens-before from the payload write to its read.
With no later payload writes, the reader gets 42.
Replacing either flag ordering with Relaxed removes this publication
guarantee. It does not guarantee a visible failure on a particular run.
The payload itself is atomic here so that a weakened version would not
introduce a non-atomic data race. For non-atomic shared data, missing
synchronization can cause undefined behavior, not merely a stale value.
This example assumes one publication and no reset of the flag. Reusing it for repeated messages needs another protocol to prevent reads and writes from overlapping. The loop also has no bounded waiting guarantee; a channel is usually a better choice for an application waiting for a value.
Where sequential consistency helps
SeqCst is a legitimate choice when you need a common order across several
atomics, and it can make an initial design easier to reason about. It is not
evidence that the author made a mistake. It also does not make a sequence of
separate operations into one transaction or fix an invalid ownership design.
Consider this example with both atomic variables initially zero:
Thread 0: x.store(1, SeqCst); read y with SeqCst
Thread 1: y.store(1, SeqCst); read x with SeqCst
Both loads cannot return zero. In a common order respecting each thread's operation order, one of the stores must come before the other thread's load. With all four operations relaxed, the memory model permits both reads to return zero.
Do not weaken orderings just to follow a style rule. First identify which
guarantee the algorithm uses, then measure whether changing the ordering is
useful on the targets you support. Instruction costs depend on the operation,
compiler and architecture; SeqCst does not universally mean an extra fence.
Test the code and explain the reasoning
Stress tests can expose bugs, but successful runs cover only the executions they observed. Some orderings compile to identical instructions on a given target, while different compilers or targets may expose different behavior. Identical instructions do not make two source-level guarantees equivalent.
Loom explores executions of code written using its modeled synchronization types, subject to its supported behaviors and configured bounds. Miri and sanitizers can detect classes of invalid execution. These tools complement review; none is a blanket proof that an arbitrary concurrent program is correct.
Check your understanding A colleague ran a lock-free queue a million times without failures. Why is that insufficient evidence of correctness? Show answer Hide answer
Those runs are useful evidence about the executions tested. They do not cover every permitted schedule, compiler transformation or memory-ordering behavior. Review the queue's ownership and synchronization argument, then use targeted tests and suitable concurrency tools to look for counterexamples. Neither the processor brand nor the number of successful runs replaces that argument.
The companion store-buffer interpreter makes a selected execution reproducible under a small set of teaching rules. It does not implement the complete Rust memory model and cannot validate a real lock-free algorithm by itself.
Valid orderings are only the first check
Loads accept Relaxed, Acquire and SeqCst; stores accept Relaxed,
Release and SeqCst. Read-modify-write operations such as fetch_add
accept all five. An acquire-only read-modify-write has a relaxed write part;
a release-only one has a relaxed read part.
compare_exchange takes two orderings: one for a successful read-modify-write,
and one for a failed comparison, which performs only a load. Failure therefore
accepts Relaxed, Acquire or SeqCst, not Release or AcqRel. Rust does
not require the failure ordering to be weaker than the success ordering.
The deny-by-default invalid_atomic_ordering lint catches invalid orderings
when it can determine them. Invalid values supplied at runtime can panic.
A valid combination still needs a reasoning argument for the algorithm.
Atomic fences provide another way to establish synchronization, through specific relationships with atomic accesses. Two fences alone do not publish arbitrary shared data. Use them only with a documented synchronization argument; they are not a general replacement for correctly ordered accesses.
For each protocol, write down the shared state, the operation that publishes it, the operation that observes that publication, and what prevents conflicting accesses afterward. That explanation is more useful than a comment that merely repeats the ordering's name.