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. only scoped to a single atomic rather than the whole program.
Other than an edge case involving `SeqCst` mixed with weaker orderings (detailed Other than an edge case involving `SeqCst` mixed with weaker orderings (detailed
in the next section), _S_ is primarily controlled by the happens-before later on), _S_ is primarily controlled by the happens-before relations in a
relations in a program: this means that if an action _A_ happens-before an program: this means that if an action _A_ happens-before an action _B_, it is
action _B_, it is also guaranteed to appear before _B_ in _S_. Other than that also guaranteed to appear before _B_ in _S_. Other than that restriction, _S_ is
restriction, _S_ is unspecified and will be chosen arbitrarily during execution. unspecified and will be chosen arbitrarily during execution.
Once a particular _S_ has been established, every atomics modification order is 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 then guaranteed to be consistent with it, so a `SeqCst` load will never see a
never see a value that has been overwritten by a write that occurred before it value that has been overwritten by a write that occurred before it in _S_, or a
in _S_, or a value that has been written by a write that occured after it in value that has been written by a write that occured after it in _S_ (note that a
_S_ (note that a `Relaxed`/`Acquire` load however might, since there is no `Relaxed`/`Acquire` load however might, since there is no “before” or “after” as
“before” or “after” as it is not in _S_ in the first place). 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 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 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 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 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 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 as `true` and precedes both final loads in _S_, so that the coherence ordering
for this since although they “know” that their corresponding atomic is `true` guarantee will apply (the final loads themselves dont work for this since
they dont interact with it directly so _S_ doesnt care). 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, 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, 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 `X` (gives `true`)
1. `c` loads `Y` (required to be `true`) 1. `c` loads `Y` (required to be `true`)
All the places were the load is requied to give `true` were caused by a 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`, because otherwise _S_ preceding load in _S_ of the same atomic which saw `true` — otherwise, the load
would be inconsistent with the atomics modification order and that is would be coherence-ordered-before a load which precedes it in _S_, and that is
impossible. impossible.
## The mixed-`SeqCst` special case ## 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 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 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 precede A in _S_ (and they are not both loads) then that means C is always
both access, C would have to come before A. Lets say then that C loads from `x` coherence-ordered-before A. Lets say then that C loads from `x` (the atomic
(the atomic that A has to access), it may load the value that came before A if that A has to access), it may load the value that came before A if it were to
it were to precede A in _S_: precede A in _S_:
```text ```text
t_1 x t_2 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 Ah wait no, that doesnt work because regular coherence still mandates that `1`
only value that can be loaded. In fact, once `1` is loaded _S_s required 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 consistency with coherence orderings means that A _is_ required to precede C in
_S_ after all. _S_ after all.
So somehow, to observe this difference we need to have a _different_ `SeqCst` 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. encoded it into the specification.
Generally however, this rule is so complex its best to just avoid it entirely 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 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.

Loading…
Cancel
Save