liveBook · Manning

Release barrier

As explained in Latency

A release memory barrier prevents preceding memory operations from being reordered after a synchronization operation and makes their effects available to an acquiring observer.

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

How Latency covers it

How deep it goes
Defines release ordering, maps it to lower-level barriers, and works through a failed two-thread invariant and a queue implementation.
Examples
Uses a completion flag and data value, then Rust queue push and pop operations with atomic front and back indexes.
What it assumes
Assumes familiarity with threads, shared-memory reads and writes, and Rust atomics; defines the ordering role of release and acquire.
What you can do after
Build it from scratch in a Rust single-producer, single-consumer queue by pairing release index stores with acquire index loads.
How it connects
Links acquire ordering, relaxed ordering, memory reordering, atomic operations, and wait-free single-producer, single-consumer queues.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Latency. The section links point to the parts of the book it draws on.