liveBook · Manning

Acquire semantics

As explained in C++ Concurrency in Action, Second Edition

An acquire operation is an atomic operation whose ordering can synchronize with a release operation on the same memory location.

Acquire semantics give an atomic operation a way to receive ordering from a release operation on the same atomic object. When the operations synchronize, work sequenced before the release happens before work sequenced after the acquire. This can make earlier writes to non-atomic data safe to read in another thread; the data itself need not be atomic. section 5.3.7

Acquiring a spinlock

A spinlock can loop on flag.test_and_set(std::memory_order_acquire) until it returns false. The thread that cleared the flag with flag.clear(std::memory_order_release) has finished its protected writes. That clear synchronizes with the successful acquisition, so those writes happen before the new lock holder accesses the protected data. Calls that find the flag set keep looping; they have not acquired the lock. section 5.3.7 section 5.2.2

Choose acquire for the operation that must establish ordering before a later read, not simply for every atomic access. Loads and read-modify-write operations can use std::memory_order_acquire; a store such as atomic_flag::clear() cannot. Without an explicit order, atomic operations default to std::memory_order_seq_cst. section 5.2.1 section 5.2.2

Reading a published stack node

In a lock-free stack, push() initializes a node and publishes it through a release compare-exchange on head. Before pop() reads the node's next field, a successful compare_exchange_strong() in increase_head_count() uses acquire ordering. It synchronizes with publication of the pointer it reads, establishing that the node initialization happens before the read. A failed exchange retries and can use relaxed ordering; the initial head load can also be relaxed. Once that acquire has established ordering, the later exchange in pop() need not repeat it merely to read the node's data. section 7.2.5

Listing 5.1. Implementation of a spinlock mutex using std::atomic_flag
class spinlock_mutex
{
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}
    void lock()
    {
        while(flag.test_and_set(std::memory_order_acquire));
    }
    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
};

The complete spinlock places acquire on the successful test_and_set() path and release on clear(). section 5.2.2

void increase_head_count(counted_node_ptr& old_counter)
{
    counted_node_ptr new_counter;
    do
    {
        new_counter=old_counter;
        ++new_counter.external_count;
    }
    while(!head.compare_exchange_strong(old_counter,new_counter,
        std::memory_order_acquire,std::memory_order_relaxed));
    old_counter.external_count=new_counter.external_count;
}

The head-count exchange uses acquire on success and relaxed ordering on failure, matching the point where pop() needs the published node. section 7.2.5

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Works through happens-before chains for non-atomic data in a spinlock and ordering choices across stack compare-exchanges.
Examples
Uses an atomic_flag spinlock and a reference-counted lock-free stack with differently ordered successful and failed exchanges.
What it assumes
Introduces atomic operation categories and memory-order arguments before applying them to cross-thread data access.
What you can do after
Apply it via std::atomic_flag or compare_exchange_strong to establish ordering before reading protected or published data.
How it connects
Connects release semantics, happens-before, atomic flags, compare-exchange, spinlocks, and lock-free stacks.

Also explained in

Compare how 5 books explain acquire semantics →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.