Namespaces
Variants
Views
Actions

Template:cpp/atomic/sequentially-consistent

From cppreference.com

Atomic operations tagged memory_order_seq_cst not only order memory the same way as release/acquire ordering (everything that happened-before a store in one thread becomes a visible side effect in the thread that did a load), but also establish a single total modification order of all atomic operations that are so tagged.

Formally,

each memory_order_seq_cst operation B that loads from atomic variable M, observes one of the following:

  • the result of the last operation A that modified M, which appears before B in the single total order,
  • OR, if there was such an A, B may observe the result of some modification on M that is not memory_order_seq_cst and does not happen-before A,
  • OR, if there wasn't such an A, B may observe the result of some unrelated modification of M that is not memory_order_seq_cst.

If there was a memory_order_seq_cst std::atomic_thread_fence operation X sequenced-before B, then B observes one of the following:

  • the last memory_order_seq_cst modification of M that appears before X in the single total order,
  • some unrelated modification of M that appears later in M's modification order.

For a pair of atomic operations on M called A and B, where A writes and B reads M's value, if there are two memory_order_seq_cst std::atomic_thread_fences X and Y, and if A is sequenced-before X, Y is sequenced-before B, and X appears before Y in the Single Total Order, then B observes either:

  • the effect of A,
  • some unrelated modification of M that appears after A in M's modification order.

For a pair of atomic modifications of M called A and B, B occurs after A in M's modification order if

  • there is a memory_order_seq_cst std::atomic_thread_fence X such that A is sequenced-before X and X appears before B in the Single Total Order,
  • or, there is a memory_order_seq_cst std::atomic_thread_fence Y such that Y is sequenced-before B and A appears before Y in the Single Total Order,
  • or, there are memory_order_seq_cst std::atomic_thread_fences X and Y such that A is sequenced-before X, Y is sequenced-before B, and X appears before Y in the Single Total Order.

Note that this means that:

1) as soon as atomic operations that are not tagged memory_order_seq_cst enter the picture, the sequential consistency is lost,
2) the sequentially-consistent fences are only establishing total ordering for the fences themselves, not for the atomic operations in the general case (sequenced-before is not a cross-thread relationship, unlike happens-before).
(until C++20)
Formally,

an atomic operation A on some atomic object M is coherence-ordered-before another atomic operation B on M if any of the following is true:

1) A is a modification, and B reads the value stored by A,
2) A precedes B in the modification order of M,
3) A reads the value stored by an atomic modification X, X precedes B in the modification order, and A and B are not the same atomic read-modify-write operation,
4) A is coherence-ordered-before X, and X is coherence-ordered-before B.

There is a single total order S on all memory_order_seq_cst operations, including fences, that satisfies the following constraints:

1) if A and B are memory_order_seq_cst operations, and A strongly happens-before B, then A precedes B in S,
2) for every pair of atomic operations A and B on an object M, where A is coherence-ordered-before B:
a) if A and B are both memory_order_seq_cst operations, then A precedes B in S,
b) if A is a memory_order_seq_cst operation, and B happens-before a memory_order_seq_cst fence Y, then A precedes Y in S,
c) if a memory_order_seq_cst fence X happens-before A, and B is a memory_order_seq_cst operation, then X precedes B in S,
d) if a memory_order_seq_cst fence X happens-before A, and B happens-before a memory_order_seq_cst fence Y, then X precedes Y in S.

The formal definition ensures that:

1) the single total order is consistent with the modification order of any atomic object,
2) a memory_order_seq_cst load gets its value either from the last memory_order_seq_cst modification, or from some non-memory_order_seq_cst modification that does not happen-before preceding memory_order_seq_cst modifications.

The single total order might not be consistent with happens-before. This allows more efficient implementation of memory_order_acquire and memory_order_release on some CPUs. It can produce surprising results when memory_order_acquire and memory_order_release are mixed with memory_order_seq_cst.

For example, with x and y initially zero,

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

is allowed to produce r1 == 1 && r2 == 3 && r3 == 0, where A happens-before C, but C precedes A in the single total order C-E-F-A of memory_order_seq_cst (see Lahav et al).

Note that:

1) as soon as atomic operations that are not tagged memory_order_seq_cst enter the picture, the sequential consistency guarantee for the program is lost,
2) in many cases, memory_order_seq_cst atomic operations are reorderable with respect to other atomic operations performed by the same thread.
(since C++20)

Sequential ordering may be necessary for multiple producer-multiple consumer situations where all consumers must observe the actions of all producers occurring in the same order.

Total sequential ordering requires a full memory fence CPU instruction on all multi-core systems. This may become a performance bottleneck since it forces the affected memory accesses to propagate to every core.