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:
- A is sequenced-before B.
- 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:
- A is sequenced-before B.
- A synchronizes-with B.
- 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:
- A happens-before B.
- 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
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:
- A is sequenced-before B.
- 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:
- A is sequenced-before B.
- A synchronizes-with B.
- 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:
1 is sequenced before 2, and therefore 1 happens-before 2;
2 carries a dependency to 3, thus 2 inter-thread happens-before 3, thus 2 happens-before 3
3 is sequenced before 4, thus 3 happens-before 4.
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.