I recently encountered two seemingly opposing explanations on the transitivity of acquire-release semantics. The section "Transitive Synchronization with Acquire-Release Ordering" on pg 160 of Concurrency in Action by A. Williams (see below) directly contradicts GCC's Wiki on memory synchronization modes (also below). In the book, it basically says thread synchronization is transitive across 3 threads even if the third thread doesn't directly synchronize with the first thread's variables. gnu.org's website says the opposite.
In gnu.org's GCC Wiki, the section "Overall Summary" it gives the following example and explanation.
-Thread 1- -Thread 2- -Thread 3-
y.store (20); if (x.load() == 10) { if (y.load() == 10)
x.store (10); assert (y.load() == 20) assert (x.load() == 10)
y.store (10)
}
Release/acquire mode only requires the two threads involved to be synchronized. This means that synchronized values are not commutative to other threads. The assert in thread 2 must still be true since thread 1 and 2 synchronize with x.load(). Thread 3 is not involved in this synchronization, so when thread 2 and 3 synchronize with y.load(), thread 3's assert can fail. There has been no synchronization between threads 1 and 3, so no value can be assumed for 'x' there.
The description is about Acquire/Release modification of the code shown above. This modification entail all .store
calls are replaced with stores with release semantic, and all .load
calls are replaced with loads with acquire semantic.
Source: https://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync
On pg. 159-160 of Concurrency in Action, there's an example illustrating thread synchronization is transitive across 3 threads even if the third thread doesn't directly synchronize with the first thread's variables.
std::atomic<int> data[1];
std::atomic<bool> sync1(false),sync2(false);
void thread_1()
{
data[0].store(42,std::memory_order_relaxed);
sync1.store(true,std::memory_order_release); // Set sync 1
}
void thread_2()
{
while(!sync1.load(std::memory_order_acquire)) // Loop until sync1 is set
sync2.store(true,std::memory_order_release); // Set sync2
}
void thread_3()
{
while(!sync2.load(std::memory_order_acquire)); // Loop until sync2 is set
assert(data[0].load(std::memory_order_relaxed)==42);
}
acquire-release ordering can be used to synchronize data across several threads, even when the “intermediate” threads haven’t touched the data.
... Because of the transitive nature of happens-before, you can chain it all together: the stores to data happen before the store to sync1, which happens before the load from sync1, which happens before the store to sync2, which happens before the load from sync2, which happens before the loads from data. Thus the stores to data in thread_1 happen before the loads from data in thread_3, and the asserts can’t fire.
Source: https://beefnoodles.cc/assets/book/C++%20Concurrency%20in%20Action.pdf
I understood "can't fire" from the book's excerpt to means it does not evaluate to false. Which one is correct?
First of all, for the future readers, the first example implies acquire/release despite it not being spelled in the code (that part of the wiki talks about how different orders affect this snippet).
Yes, the wiki seems wrong. While "synchronization" itself isn't transitive (it narrowly refers to one thread acquire-reading what another thread has release-written), it doesn't matter, because "happens before" (which is caused by the synchronization) is transitive. (I'm conflating "{inter-thread,strongly,simply} happens before" here, see this.)