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:
Will it work on ARM?
Do you know any architectures where it won't work?
Assuming the code will, does changing memory order for operations on
bvariable to relaxed make the code be wrong? IMO yes.Does changing memory order of operations on
avariable to release store and acquire load, without chaning it forb, may impact the behavior?Does chaning operations on
ato sequentially consistent changes the behavior? As I understand memory_order_seq_cst imposes common relative order of atomics states for all threads, so if bothaandbwere memory_order_seq_cst, the code would be ok also in theory, but is it the same ifboperations 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:
head == tail (queue empty)
thread A pushes a node
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))
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