C++ Concurrency – Are Acquire-Release Memory Order Semantics Transitive?

cconcurrency

According to cppreference, we can use release-acquire semantics to ensure write-read precedence between two threads as follows:

// Thread A
1. Write to X
2. Atomic store to Y with std::memory_order_release
// Thread B
3. Atomic load at Y with std::memory_order_acquire
4. Read at X

Supposing that 2 is ordered before 3 (take it as a given that this occurs according to Y), we are guaranteed that A's write to X is visible to B since we can establish a total order on the events (as conveniently described by my enumeration above) because 1 sequenced-before 2 synchronizes-with 3 sequenced-before 4 and because release-acquire ensure that all of A's writes are visible to B after 3.

However, the link above explicitly states that in this situation "all memory writes (non-atomic and relaxed atomic) [by A become] visible side-effects in thread B." My question is then: are "visible side-effects" considered writes made B to threads previously unaware of A's writes?

In other words, suppose I had a third thread C and another atomic:

// Thread A
1. Write to X
2. Atomic store to Y with std::memory_order_release
// Thread B
3. Atomic load at Y with std::memory_order_acquire
4. Read at X
5. Atomic store to Z with std::memory_order_release
// Thread C
6. Atomic load at Z with std::memory_order_acquire
7. Read at X

(Assume similarly that 5 happens-before 6). Would C read A's write to X? Or is B's "visible side-effect" on X not considered a write?

Best Answer

Formally, you are asking if the side effect on X in thread A is visible with respect to value computation of X in thread C.

For that to be true, 1 needs to happen-before 7.

A happens-before B is defined, as synchronizes-with or inter-thread happens-before, and the latter is defined as

for some evaluation X,

— A synchronizes with X and X is sequenced before B, or

— A is sequenced before X and X inter-thread happens before B, or

— A inter-thread happens before X and X inter-thread happens before B.

So yes, it is perfectly transitive across multiple threads. In particular, in your case,

1 sequenced-before 2

2 synchronizes-with 3

3 sequenced-before 5

5 synchronizes-with 6

6 sequenced-before 7

Therefore, 1 happens-before 7, therefore 1 is a visible side-effect with respect to 7, and therefore the value of non-atomic scalar object X as determined by the value computation of X (your "Read at X") is guaranteed to be the value stored by the visible side-effect 1.