Does thread A seeing one atomic variable loaded with acquire and stored by thread B with release, see second atomic variable assuming thread B saw it
10:10 29 Aug 2026

My question boils down to:

std::atomic a{0};
std::atomic b{0};
// thread A
a.store(1, memory_order_relaxed);

// thread B
while(a.load(memory_order_relaxed) != 1) continue;
b.store(2, memory_order_release);

// thread C
while(b.load(memory_order_acquire) != 2) continue;
assert(a.load(memory_order_relaxed) == 1); // can it fail?

So IMO in theory it can fail. Interestingly this paper (6.3) presents a similar example, stating that it works on most mainstream computer systems.

Another questions are:

  1. Will it work on ARM?

  2. Do you know any architectures where it won't work?

  3. Assuming the code will, does changing memory order for operations on b variable to relaxed make the code be wrong? IMO yes.

  4. Does changing memory order of operations on a variable to release store and acquire load, without chaning it for b, may impact the behavior?

  5. Does chaning operations on a to sequentially consistent changes the behavior? As I understand memory_order_seq_cst imposes common relative order of atomics states for all threads, so if both a and b were memory_order_seq_cst, the code would be ok also in theory, but is it the same if b operations are as in the example release and acquire?

Here is a real example from a lock free queue, I got inspired by Anthony Williams book (example adopted a bit):

struct node;
struct counted_node_ptr
{
    int external_count;
    node* ptr;
}
atomic head;
atomic tail;
void pop() // simplified to return void
{
    counted_node_ptr old_head = head.load(memory_order_relaxed);
    counted_node_ptr new_head;
    do
    {
        new_head = old_head;
        new_head.external_count++;
    }
    while(!head.compare_exchange_strong(old_head, new_head,
                                        memory_order_acquire,
                                        memory_order_relaxed); // (1)
    old_head = new_head;
    node* ptr = old_head.ptr;    
    if(ptr == tail.load(memory_order_acquire).ptr) // (2)
    {
        // Do something, doesn't matter here what
        return;
    }
    if(head.compare_exchange_strong(old_head, ptr->next,
                                    memory_order_release, memory_order_relaxed) // (3)
    {
        // Do something
    }
    
}

The scanario here is:

  1. head == tail (queue empty)

  2. thread A pushes a node

  3. thread B pops, i.e. changes head in (3) (memory order release). Since it succeeded to pop, it must have seen the pushed node (so it saw tail != head in (2))

  4. thread C calls pop, and sees the new head in (1), but doesn't see the new tail in (2)

So the tail is counterpart of a in the example at the top (although with different memory order) and head is b

c++ multithreading stdatomic