c++atomicstdatomicmemory-model

Does release-consume order violate sequence-before order?


The following code is very easy illustrating the release-consume ordering:


    std::atomic<int> a{ 0 };
    std::atomic<bool> b{ false };
    void t1()
    {
        a.store(1, std::memory_order_relaxed);  //1
        b.store(true, std::memory_order_release); //2
    }
    void t2()
    {
        while (!b.load(std::memory_order_consume)); //3
        assert(a.load(std::memory_order_relaxed) == 1);  //4
    }

The standard says the assert in line 4 may fire because b.load() does not carry dependency into a.load().

However, according the definition of sequence-before,

Each full-expression is sequenced before the next full-expression.

and the sequence-before introduce happens-before,

Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:

  1. A is sequenced-before B.
  2. A inter-thread happens before B.

line 3 is a full expression before line 4, which means line 3 is sequenced-before line 4, and as a consequence, line 3 happens-before line 4.

Based on the same rules, line 1 is sequenced-before line 2, so line 1 happens-before line 2.

With line 2 inter-thread happens before line 3 because of dependency-ordered before, a happens-before chain is formed: line 1->line 2->line 3->line 4, as

Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:

  1. A is sequenced-before B.
  2. A synchronizes-with B.
  3. A happens-before X, and X happens-before B.

So, the side effect of line 1 should be visible to line 4, as

The side-effect A on a scalar M (a write) is visible with respect to value computation B on M (a read) if both of the following are true:

  1. A happens-before B.
  2. There is no other side effect X to M where A happens-before X and X happens-before B.

Of course it is wrong. But I don't know where is the key that leads to this incorrect result, as each step fully obeys the formal definitions.

All the quote above is from https://en.cppreference.com/w/cpp/atomic/memory_order.html#Release-Consume_ordering


Solution

  • cppreference tries to describe all versions of the C++ standard on the same page (see the green labels in the right margin), and I think you've got them mixed. You've quoted two different definitions of "happens before".

    The first definition you quoted is for C++23 and earlier:

    [C++23] Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:

    1. A is sequenced-before B.
    2. A inter-thread happens before B.

    But the second is for C++26:

    [C++26] Regardless of threads, evaluation A happens-before evaluation B if any of the following is true:

    1. A is sequenced-before B.
    2. A synchronizes-with B.
    3. A happens-before X, and X happens-before B.

    In C++26, consume memory ordering is removed altogether. (Compilers found it very hard to implement, and I believe all mainstream compilers just promoted consume loads to acquire.) So we cannot use the C++26 definition to reason about consume ordering. We must use the C++23 definition.

    In C++23, the happens-before relation is not transitive. It's true that:

    From this, you can say that 1 inter-thread happens-before 3, and thus 1 happens-before 3. But there is no rule allowing you to conclude that 1 happens-before 4.

    The rules are written this way precisely so that a release store happens-before a consume load that sees its value, but this happens-before relation is not inherited by operations that are merely sequenced after the consume load. They have to actually carry its dependency.