Search code examples
c++concurrencyatomicstdatomic

Happens Before relationship in the same thread


I am reading the book "C++ Concurrency in Action" section 5.3 on Memory Model of C++ and listing 5.5 below confuses me:

std::atomic<bool> x,y;
std::atomic<int> z;
void write_x_then_y()
{
   // Is there a Happens-Before relationship for statement (1) and (2) below?
   x.store(true,std::memory_order_relaxed); // --> (1)
   y.store(true,std::memory_order_relaxed); // --> (2)
}
void read_y_then_x()
{
   while(!y.load(std::memory_order_relaxed));
   if(x.load(std::memory_order_relaxed))
   ++z;
}

int main()
{
    x=false;
    y=false;
    z=0;
    std::thread a(write_x_then_y);         std::thread b(read_y_then_x);
    a.join();
    b.join();
    assert(z.load()!=0);
}

What confuses me is function void write_x_then_y(). The author says "This time the assert at the end of main() can fire" and statement (1) on atomic variable x has a "happens-before" relationship with statement (2) on atomic variable y. But I thought with std::memory_order_relaxed, there is no synchronization between different atomic variables, and compiler/CPU can reorder these two statements without violating the program semantics.

The happensbefore relationships from listing 5.5 are shown in figure 5.4, along with a possible outcome. enter image description here


Solution

  • An evaluation which is sequenced-before another evaluation also happens-before it.

    Sequenced-before is a relation between evaluations in the same thread and independent of synchronization mechanism like e.g. atomic load/stores with non-relaxed memory orders.

    Happens-before just extends the sequenced-before relation for evaluation order from the single-threaded case to the multi-threaded case by incorporating synchronization mechanisms between threads.

    See https://en.cppreference.com/w/cpp/atomic/memory_order for exact definitions.

    An evaluation in one statement preceding another statement is always sequenced-before the latter. (Basically there is a sequence point between two statements, although that term is not used anymore since C++11.)


    The compiler can reorder the two relaxed stores. That doesn't affect the happens-before relation though. Just because two stores (on different atomics) happen-before one another does not imply that the stores can't be observed in a different order by another thread, as demonstrated by the example in the diagram.

    You would need to establish a happens-before relation between the loads and stores to prevent the shown outcome. And for that inter-thread ordering in some way is required, e.g. via release/acquire atomic operations. Basically, you would need to establish some arrow betweeen the gray boxes in the diagram.