This snip is from Herb Sutter's Atomic Weapons talk slide from page number 19.
If I am understanding this correctly, what Herb is saying is that, for the assert in thread 3 to not fire, this has to follow sequential consistency. So the following code will not fire the assert.
int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics
void thread1() {
g = 1;
x.store(1, std::memory_order_seq_cst);
}
void thread2() {
if(x.load(std::memory_order_seq_cst) == 1)
y.store(1, std::memory_order_seq_cst);
}
void thread3() {
if(y.load(std::memory_order_seq_cst) == 1)
assert( g == 1 );
}
But wouldn't this also not fire the assert if release/acquire was used instead as follows?
int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics
void thread1() {
g = 1; // A
x.store(1, std::memory_order_release);
}
void thread2() {
if(x.load(std::memory_order_acquire) == 1)
y.store(1, std::memory_order_release);
}
void thread3() {
if(y.load(std::memory_order_acquire) == 1)
assert( g == 1 ); // B
}
Q1 - Doesn't //A
simply-happens-before //B
ensuring that the assert will not fire, since no other thread writes to g
?
Q2 - Am I understanding the purport of the slide incorrectly, or something is wrong on the slide?
This snip is from Herb Sutter's Atomic Weapons talk slide from page number 19.
If I am understanding this correctly, what Herb is saying is that, for the assert in thread 3 to not fire, this has to follow sequential consistency. So the following code will not fire the assert.
int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics
void thread1() {
g = 1;
x.store(1, std::memory_order_seq_cst);
}
void thread2() {
if(x.load(std::memory_order_seq_cst) == 1)
y.store(1, std::memory_order_seq_cst);
}
void thread3() {
if(y.load(std::memory_order_seq_cst) == 1)
assert( g == 1 );
}
But wouldn't this also not fire the assert if release/acquire was used instead as follows?
int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics
void thread1() {
g = 1; // A
x.store(1, std::memory_order_release);
}
void thread2() {
if(x.load(std::memory_order_acquire) == 1)
y.store(1, std::memory_order_release);
}
void thread3() {
if(y.load(std::memory_order_acquire) == 1)
assert( g == 1 ); // B
}
Q1 - Doesn't //A
simply-happens-before //B
ensuring that the assert will not fire, since no other thread writes to g
?
Q2 - Am I understanding the purport of the slide incorrectly, or something is wrong on the slide?
Share Improve this question edited Feb 6 at 8:31 Dhwani Katagade asked Feb 5 at 10:54 Dhwani KatagadeDhwani Katagade 1,29215 silver badges28 bronze badges 4 |2 Answers
Reset to default 9Yes, acquire/release ordering would be sufficient here.
With acquire/release ordering and g
being non-atomic, the code is okay because
- T1:
g = 1
is sequenced before T1:x = 1
- T1:
x = 1
synchronizes with T2:x == 1
- T2:
x == 1
is sequenced beforeT2:y = 1
- T2:
y = 1
is synchronizes with T3:y == 1
- T3:
y = 1
is sequenced before T3:g == 1
You have a happens before relationship running all the way from T1:g = 1
to T3:g == 1
, and this means that there is no data race and that T3 has to read 1
(see [intro.races] p13).
Obviously, Thread 3 could also "run first", synchronization doesn't happen, and this whole happens before chain doesn't exist.
However, if T3:y == 1
is true, the synchronizations must have taken place, T1:g = 1
happens before T3:assert(g == 1)
, and the assertion cannot fail.
Herb's mistake (possibly)
In the talk, Herb mentions that g
is non-atomic.
That makes it really hard to tell why he thinks that sequential consistency is required, considering that sequentially consistent operations on x
don't provide any relevant extra guarantees regarding g
.
Perhaps if g
was atomic, one might suspect that g == 1
wouldn't always be true because of [intro.races] p13:
The value of an atomic object M, as determined by evaluation B, is the value stored by some unspecified side effect A that modifies M, where B does not happen before A.
In other words, assert(g == 1)
could read arbitrarily far into the past modifications of g
(but not into the future).
This is the rule that misled Herb into thinking that this example requires sequential consistency.
However, a happens before chain as described above also imposes requirements on atomics:
If a side effect X on an atomic object M happens before a value computation B of M, then the evaluation B takes its value from X or from a side effect Y that follows X in the modification order of M.
In our case, g = 1
happens before assert(g == 1)
, so the assertion has to pass (or take a value of g
that is written after g = 1
, but no such write exists in this sample code).
Note: a happens before chain is upheld because an alternating pattern of sequenced before and synchronizes with is covered by inter-thread happens before.
The example is important, but you're right the heading and discussion should be updated now that C++11 "acq/rel" meanings are well established.
This example can't affected by "ordinary acq/rel" vs "SC acq/rel" because the key difference between the two is that the former allows store-load reordering and the latter disallows it, as I covered a few slides earlier. This example has all the other three combinations, with one thread each for store-store, load-store, and load-load; but this example doesn't have store-load and so it's immune to the difference between those two acq/rel flavors.
The main point I was aiming for in this slide (whose second half covers a total-store-order example) is that for a memory model to be usable it can't just talk about individual threads' operations in isolation and sufficiently cover all cases. To have a consistent memory model a programmer can reason about, each thread's operations also have to work in an SC manner both transitively (when daisy-chained together like this) and globally (for a total store order, which was the other example on this slide), otherwise we can violate causality and humans can't write a coherent program.
I think the reason I mentioned "acq/rel" in the slide title is that during the development of the C++ MM there were many proposals for more relaxed rules than we standardized, including ideas about just letting individual pairs of acq/rel be enough without a transitive property, and those models are too relaxed to be useful. But I agree that since now "acq/rel" are now well-established to mean the m_o_acq
and m_o_rel
in the standard I should stick to those meanings of "acq/rel" to avoid confusion (they were still very new at the time I gave this talk and were still being implemented by compilers, but this was 2012 and I still should not have made them the issue here).
I've updated the slide title for the next time I give the talk. Thanks!
g = 1
tog == 1
. Yes, synchronization only happens on the same atomic variable, but what ultimately matters is "happens before", which can be established in multiple ways and between different objects, atomic or otherwise. – Jan Schultke Commented Feb 5 at 12:05