A memory barrier, or memory fence, constrains the ordering of memory operations across a point in concurrent code. CPUs may execute memory operations out of order, so another thread need not observe updates in the order suggested by the source code. Locks can handle ordering implicitly; lock-free and wait-free algorithms often need more explicit control. section 8.4
Choose an ordering constraint for the operations that must remain in sequence:
- LoadLoad, StoreStore, LoadStore, and StoreLoad barriers name the kinds of operations whose reordering is prohibited across the barrier. section 8.4.1
- Acquire keeps subsequent operations from moving before it; release keeps preceding operations from moving after it. Sequential consistency provides a stronger total-order guarantee for marked operations, while relaxed ordering supplies no such ordering guarantee. section 8.4.1
- Account for the machine memory model: x86-64 uses total store order, while ARM64 permits more extensive reordering. A barrier may compile to no instruction on an architecture that does not need a specific barrier instruction. section 8.4.1
Publish a value before its completion flag
Suppose one thread stores 1 in value and then sets done; another reads done and value. The required invariant is: if done is true, value is 1. With relaxed operations on both variables, a repeated run exhibits done = true alongside value = 0. Store done with release ordering and load it with acquire ordering instead. The writer’s value update then precedes publication of done, and a reader that observes done as true sees that update. The value operations in the supplied Rust example remain relaxed. section 8.4.3
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
for _ in 0..1000000 {
let value = Arc::new(AtomicUsize::new(0));
let done = Arc::new(AtomicBool::new(false));
let value1 = value.clone();
let done1 = done.clone();
let t1 = thread::spawn(move || {
value1.store(1, Ordering::Relaxed);
done1.store(true, Ordering::Release);
});
let t2 = thread::spawn(move || {
let done = done.load(Ordering::Acquire);
let value = value.load(Ordering::Relaxed);
println!("value = {}, done = {}", value, done);
});
t1.join().unwrap();
t2.join().unwrap();
}
}
The Rust listing places release on the completion-flag store and acquire on its load; compare these lines with the relaxed value operations. section 8.4.3
The same publication pattern appears in a bounded single-producer, single-consumer queue. Its producer writes a buffer slot before releasing the updated back index; its consumer acquires that index before reading the slot. The consumer likewise releases its updated front index, which the producer acquires when checking space. Each side uses relaxed ordering for its own index. This replaces a mutex in the shown queue, but the ordering must be reasoned about alongside concurrent access to the buffer. section 8.6


