Acquire and release

We have just examined the acquire and release operations in the context of the lock example from last chapter. You can think of them as “one-way” barriers: an acquire operation permits other reads and writes to move past it, but only in a \(before\to after\) direction. A release works the opposite manner, allowing actions to move in an \(after\to before\) direction. On Arm and other weakly-ordered architectures, this enables us to eliminate one of the memory barriers in each operation, such that

int acquireFoo()
{
    return foo.load(memory_order_acquire);
}

\[ \text{ } \xrightarrow{\textit{becomes}} \text{ } \]

acquireFoo:
  ldr r3, <&foo>
  ldr r0, [r3, #0]
  dmb
  bx lr
void releaseFoo(int i)
{
    foo.store(i, memory_order_release);
}

\[ \text{ } \xrightarrow{\textit{becomes}} \text{ } \]

releaseFoo:
  ldr r3, <&foo>
  dmb
  str r0, [r3, #0]
  bx lr

Together, these provide \(writer\to reader\) synchronization: if thread W stores a value with release semantics, and thread R loads that value with acquire semantics, then all writes made by W before its store-release are observable to R after its load-acquire. If this sounds familiar, it is exactly what we were trying to achieve in chapter background and enforcing law and order:

int v;
std::atomic_bool v_ready(false);

void threadA()
{
    v = 42;
    v_ready.store(true, memory_order_release);
}

void threadB()
{
    while (!v_ready.load(memory_order_acquire)) {
        // wait
    }
    assert(v == 42); // Must be true
}