A release barrier prevents memory operations before it from being reordered after it. Use it when publishing a signal that another thread will read: the writes that prepare the data must be ordered before the signal. Pair the release operation with an acquire operation on that signal so a reader that observes it also sees the preceding updates. section 8.4.1 section 8.4.3
In Rust and C++, release is a higher-level memory-ordering choice rather than a request for one fixed machine instruction. It combines the LoadStore and StoreStore barrier categories; the compiler supplies the instructions needed for the target architecture. Relaxed ordering supplies no such ordering guarantee, while sequential consistency imposes stronger ordering. section 8.4.1
Publish a completed update
Suppose one thread stores 1 in `value`, then sets `done` to true. Another reads `done`, then `value`. With relaxed ordering on both operations, repeated runs can produce `done == true` and `value == 0`. Store `done` with `Ordering::Release` and load it with `Ordering::Acquire`; the `value` operations can remain relaxed. If the reader sees `done == true`, it must also see the preceding update to `value`. 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 writer’s completion flag and `Acquire` on the reader’s flag load, leaving the data operations relaxed. section 8.4.3
The same publication pattern applies to a bounded single-producer, single-consumer queue. `push` writes a buffer slot before storing the updated `back` index with release ordering; `pop` loads `back` with acquire ordering before reading that slot. In the other direction, `pop` releases its updated `front` index and `push` acquires it. The indexes are atomic; each index has only one writer. section 8.6
/// Pushes an item into the queue. Returns an error if the queue is full.
pub fn push(&self, value: T) -> Result<(), T> {
let back = self.back.load(Ordering::Relaxed);
let front = self.front.load(Ordering::Acquire);
if front + N - back == 0 {
return Err(value);
}
let ptr = self.data.as_ptr() as *mut T;
unsafe {
ptr.add(back % N).write(value);
}
self.back.store(back + 1, Ordering::Release);
Ok(())
}
The producer code shows the buffer write followed by a release store to `back`, alongside its acquire load of `front`. section 8.6
Choose the ordering at the publication point, not indiscriminately for every access. Check which thread writes each location and which acquired signal makes its updates safe to use. In the queue example, this supports concurrency without mutual exclusion; the reported queue benchmark falls from roughly a microsecond to tens of nanoseconds, but that comparison does not isolate the cost of release ordering. section 8.6
