Improve the `SeqCst` explanation

pull/378/head
SabrinaJewson 2 years ago
parent d4f8f47439
commit 5e27ed5c03
No known key found for this signature in database
GPG Key ID: 3D5438FFA5F05564

@ -106,17 +106,65 @@ do). It is in contrast to modification orders, which are similarly total but
only scoped to a single atomic rather than the whole program.
Other than an edge case involving `SeqCst` mixed with weaker orderings (detailed
in the next section), _S_ is primarily controlled by the happens-before
relations in a program: this means that if an action _A_ happens-before an
action _B_, it is also guaranteed to appear before _B_ in _S_. Other than that
restriction, _S_ is unspecified and will be chosen arbitrarily during execution.
later on), _S_ is primarily controlled by the happens-before relations in a
program: this means that if an action _A_ happens-before an action _B_, it is
also guaranteed to appear before _B_ in _S_. Other than that restriction, _S_ is
unspecified and will be chosen arbitrarily during execution.
Once a particular _S_ has been established, every atomics modification order is
then guaranteed to be consistent with it — this means that a `SeqCst` load will
never see a value that has been overwritten by a write that occurred before it
in _S_, or a value that has been written by a write that occured after it in
_S_ (note that a `Relaxed`/`Acquire` load however might, since there is no
“before” or “after” as it is not in _S_ in the first place).
then guaranteed to be consistent with it, so a `SeqCst` load will never see a
value that has been overwritten by a write that occurred before it in _S_, or a
value that has been written by a write that occured after it in _S_ (note that a
`Relaxed`/`Acquire` load however might, since there is no “before” or “after” as
it is not in _S_ in the first place).
More formally, this guarantee can be described with _coherence orderings_, a
relation which expresses which of two operations appears before the other in an
atomics modification order. It is said that an operation _A_ is
_coherence-ordered-before_ another operation _B_ if any of the following
conditions are met:
1. _A_ is a store or RMW, _B_ is a store or RMW, and _A_ appears before _B_ in
the modification order.
1. _A_ is a store or RMW, _B_ is a load, and _B_ reads the value stored by _A_.
1. _A_ is a load, _B_ is a store or RMW, and _A_ takes its value from a place in
the modification order that appears before _B_.
1. _A_ is coherence-ordered-before a different operation _X_, and _X_ is
coherence-ordered-before _B_ (the basic transitivity property).
The following diagram gives examples for the main three rules (in each case _A_
is coherence-ordered-before _B_):
```text
Rule 1 ┃ Rule 2 ┃ Rule 3
┃ ┃
╭───╮ ┌─┬───┐ ╭───╮ ┃ ╭───╮ ┌─┬───┐ ╭───╮ ┃ ╭───╮ ┌───┐ ╭───╮
│ A ├─┘ │ │ ┌─┤ B │ ┃ │ A ├─┘ │ ├───┤ B │ ┃ │ A ├───┤ │ ┌─┤ B │
╰───╯ └───┘ │ ╰───╯ ┃ ╰───╯ └───┘ ╰───╯ ┃ ╰───╯ └───┘ │ ╰───╯
┌───┬─┘ ┃ ┃ ┌───┬─┘
│ │ ┃ ┃ │ │
└───┘ ┃ ┃ └───┘
```
The only important thing to note is that for two loads of the same value in the
modification order, neither is coherence-ordered-before the other, as in the
following example where _A_ has no coherence ordering relation to _B_:
```text
╭───╮ ┌───┐ ╭───╮
│ A ├───┤ ├───┤ B │
╰───╯ └───┘ ╰───╯
```
With this terminology applied, we can use a more precise definition of
`SeqCst`s guarantee: for two `SeqCst` operations on the same atomic _A_ and
_B_, where _A_ precedes _B_ in _S_, either _A_ must be coherence-ordered-before
_B_ or they must both be loads that see the same value in the modification
order. Effectively, this one rule ensures that _S_s order “propagates”
throughout all the atomics of the program — you can imagine each operation in
_S_ as storing a snapshot of the world, so that every subsequent operation is
consistent with it.
## Applying `SeqCst`
So, looking back at our program, lets consider how we could use `SeqCst` to
make that execution invalid. As a refresher, heres the framework for every
@ -137,9 +185,10 @@ become `SeqCst`, because they need to be aware of the total ordering that
determines whether `X` or `Y` becomes `true` first. And secondly, we need to
establish that ordering in the first place, and that needs to be done by making
sure that there is always one operation in _S_ that both sees one of the atomics
as `true` and precedes both final loads (the final loads themselves dont work
for this since although they “know” that their corresponding atomic is `true`
they dont interact with it directly so _S_ doesnt care).
as `true` and precedes both final loads in _S_, so that the coherence ordering
guarantee will apply (the final loads themselves dont work for this since
although they “know” that their corresponding atomic is `true` they dont
interact with it directly so _S_ doesnt care).
There are two operations in the program that could fulfill the first condition,
should they be made `SeqCst`: the stores of `true` and the first loads. However,
@ -207,9 +256,9 @@ executions of this program:
1. `c` loads `X` (gives `true`)
1. `c` loads `Y` (required to be `true`)
All the places were the load is requied to give `true` were caused by a
preceding load in _S_ of the same atomic which saw `true`, because otherwise _S_
would be inconsistent with the atomics modification order and that is
All the places where the load was required to give `true` were caused by a
preceding load in _S_ of the same atomic which saw `true` — otherwise, the load
would be coherence-ordered-before a load which precedes it in _S_, and that is
impossible.
## The mixed-`SeqCst` special case
@ -250,10 +299,10 @@ strongly happen-before C.
But this is all highly theoretical at the moment, so lets make an example to
show how that rule can actually affect the execution of code. So, if C were to
precede A in _S_ then that means in the modification order of any atomic they
both access, C would have to come before A. Lets say then that C loads from `x`
(the atomic that A has to access), it may load the value that came before A if
it were to precede A in _S_:
precede A in _S_ (and they are not both loads) then that means C is always
coherence-ordered-before A. Lets say then that C loads from `x` (the atomic
that A has to access), it may load the value that came before A if it were to
precede A in _S_:
```text
t_1 x t_2
@ -265,9 +314,9 @@ it were to precede A in _S_:
└───┘ ╰─────╯
```
Ah wait no, that doesnt work because coherence still mandates that `1` is the
only value that can be loaded. In fact, once `1` is loaded _S_s required
consistency with modification orders means that A _is_ required to precede C in
Ah wait no, that doesnt work because regular coherence still mandates that `1`
is the only value that can be loaded. In fact, once `1` is loaded _S_s required
consistency with coherence orderings means that A _is_ required to precede C in
_S_ after all.
So somehow, to observe this difference we need to have a _different_ `SeqCst`
@ -386,6 +435,4 @@ would make atomics significantly slower. So instead, in C++20 they simply
encoded it into the specification.
Generally however, this rule is so complex its best to just avoid it entirely
by never mixing `SeqCst` and non-`SeqCst` on a single atomic in the first place
— or even better, just avoiding `SeqCst` entirely and using a stronger ordering
instead that has less complex semantics and fewer gotchas.
by never mixing `SeqCst` and non-`SeqCst` on a single atomic in the first place.

Loading…
Cancel
Save