c++concurrencyatomicmemory-barriers

C++ happens-before relationship and memory_order_consume


Consider the following example of using std::memory_order_consume to synchronize data:

struct X
{
    int i;
    std::string s;
};

std::atomic<X*> p;
std::atomic<int> a;

void create_x()
{
    X* x=new X;
    x->i=42;
    x->s="hello";
    a.store(99,std::memory_order_relaxed);                     //1
    p.store(x,std::memory_order_release);                      //2
}

void use_x()
{
    X* x;
    while(!(x=p.load(std::memory_order_consume)))              //3
        std::this_thread::sleep(std::chrono::microseconds(1));

    assert(x->i==42);                                          //4
    assert(x->s=="hello");                                    // 5

    assert(a.load(std::memory_order_relaxed)==99);            // 6
}

int main()
{
    std::thread t1(create_x);
    std::thread t2(use_x);
    t1.join();
    t2.join();
}

Anthony Williams in his book "Concurrency in Action" gives the following explanation:

Even though the store to a 1 is sequenced before the store to p 2, and the store to p is tagged memory_order_release, the load of p 3 is tagged memory_order_consume. This means that the store to p only happens before those expressions that are dependent on the value loaded from p. This means that the asserts on the data members of the X structure (4 and 5) are guaranteed not to fire, because the load of p carries a dependency to those expressions through the variable x. On the other hand, the assert on the value of a 6 may or may not fire; this operation isn’t dependent on the value loaded from p, and so there’s no guarantee on the value that’s read. This is particularly apparent because it’s tagged with memory_order_relaxed.

This explanation makes sense, but when I try to think about it in terms of happens-before relationships as defined by cppreference, I am failing to conclude the same:

The last statement seems to contradict what is written in the book. Is there a mistake in my logical chain or the fact that the store 1 happens-before the load 6 does not necessarily mean that the effects of 1 are visible to 6?


Solution

  • Your logical error is in the last step. The definition of happens-before in C++23 (N4950 final draft) is:

    An evaluation A happens before an evaluation B (or, equivalently, B happens after A) if:

    — A is sequenced before B, or

    — A inter-thread happens before B.

    Notice there is not a third clause saying "if A happens before X and X happens before B". The happens-before relation is not defined as transitive.

    So it's true that 1 happens before 3 and 3 happens before 6, but it does not follow that 1 happens before 6.

    Notice too that you cannot show that 1 inter-thread happens before 6.

    (When there are no consume operations, so that no operation is dependency-ordered before any other, then you can show that happens-before is transitive, and is the transitive closure of the union of sequenced-before and synchronizes-with. This matches how simply-happens-before is defined, and there is a note pointing this out.)