Chapter 5: Chapter 5. The C++ memory model and operations on atomic types
This introduction explains why C++ needs a multithreading-aware memory model, when programmers need its details, and how the chapter will approach atomic synchronization.
After this chapter you will be able to
- Memory model foundation. Explain that the C++ memory model defines how the building blocks of multithreading work, making higher-level facilities reliable, while programmers using mutexes and event-signaling facilities usually need not study its details.
- Atomic synchronization and chapter path. Describe atomic types and operations as C++ facilities for low-level, close-to-the-machine synchronization, often implemented with one or two CPU instructions. State that the chapter moves from memory-model basics to atomic types and operations, then to synchronization using them; these details chiefly matter when writing code that uses atomics for synchronization, such as lock-free data structures.
- Memory model starting point. Distinguish the memory model’s structural aspects, concerning how things are laid out in memory, from its concurrency aspects. Explain that the section starts with C++ objects and memory locations because structure matters to concurrency, especially low-level atomic operations.
- Objects and memory locations. Explain that a C++ object is a region of storage with a type and lifetime, and that an object may contain sub-objects and occupy multiple memory locations. State that each scalar object occupies one memory location, even when adjacent to other scalars or in an array.
- Bit-field memory location boundaries. Describe how adjacent bit fields are distinct objects that share one memory location, while a zero-length bit field separates the following bit field into another memory location without occupying one itself.
- Enforced ordering between accesses. Explain that conflicting accesses need an enforced ordering, which may differ between runs, and that locking the same mutex or using the synchronization properties of atomic operations can establish it. State that when more than two threads access a location, each pair of accesses needs a defined ordering.
- Data races and atomic access. State the section's conditions for a data race: unordered accesses from separate threads to one memory location, with at least one write and at least one non-atomic access, cause undefined behavior. Distinguish this from atomic accesses whose order of arrival remains unspecified but whose behavior is defined.
- Per-object modification order. Explain that an object’s modification order contains its initialization and every write to it from all threads, and that all threads must agree on that order in a given execution. Distinguish this per-object agreement from any required relative order between operations on separate objects.
- Synchronization responsibility for shared objects. Explain that programmers must provide sufficient synchronization for non-atomic objects so threads agree on their modification orders; incompatible sequences for one variable constitute a data race and undefined behavior. State that atomic operations instead place the necessary synchronization responsibility on the compiler.
- Later-value observation constraints. Describe the constraints imposed once a thread has seen a value in an object’s modification order: its later reads cannot return earlier values, and its later writes to that object must occur later in the order. Explain that a read following a write to the same object in one thread must return that write’s value or a later value.
- Atomic operations and reads. Explain that an atomic operation cannot be observed half-done and that an atomic load, when all modifications to the object are atomic, reads either its initial value or a value stored by a modification. State that C++ generally requires an atomic type to obtain an atomic operation.
- Non-atomic observations and data races. Describe how another thread may observe a non-atomic operation only partly completed, including a mixture of values when an operation consists of multiple atomic operations. Explain that unsynchronized access to non-atomic variables can constitute a data race and cause undefined behavior.
- Lock-free status of atomic types. Explain that standard atomic operations are atomic by the language definition but may use an internal lock, and describe how to check whether a type is lock-free.
- Atomic operation interfaces and returns. Describe the common atomic operations, how available operations depend on the underlying type, and why their return values are values rather than references.
- Memory-order choices by operation. Distinguish the memory-order values permitted for stores, loads, and read-modify-write operations, and state the default when no order is supplied.
- Flag operations and ordering. Explain that clear() clears the flag and test_and_set() sets it while returning its previous value. Distinguish their memory-order options: clear() is a store that cannot use acquire or acq_rel, test_and_set() is a read-modify-write operation that can use any ordering tag, and both default to seq_cst.
- Spin-lock use and limits. Trace how a spin-lock mutex starts with a clear flag, repeatedly calls test_and_set() until its previous value was false to lock, and calls clear() to unlock. Explain that it provides mutual exclusion and can work with std::lock_guard<>, but busy-waits under contention; its lack of a nonmodifying query also makes std::atomic<bool> better for a general Boolean flag.
- Atomic Boolean operations. Describe how std::atomic<bool> can be initialized and updated from a plain bool, and distinguish store(), load(), and exchange() from the more limited operations on std::atomic_flag.
- Compare-exchange success and failure. Trace what compare_exchange_weak() or compare_exchange_strong() does when the atomic value matches—or does not match—the supplied expected value.
- Weak and strong retry choices. Distinguish spurious failure in compare_exchange_weak() from failure in compare_exchange_strong(), and explain how that affects retry loops.
- Atomic pointer arithmetic returns. Explain how fetch_add(), fetch_sub(), and the arithmetic operators move an atomic pointer by elements, and distinguish the pointer returned by fetch_add() from the pointer returned by +=.
- Pointer arithmetic memory ordering. Distinguish the memory-ordering options for pointer arithmetic function calls from those for operator forms, and state why fetch_add() and fetch_sub() can participate in a release sequence.
- Atomic integral operation set. Describe the arithmetic and bitwise operations available on atomic integral types beyond load, store, exchange, and compare-exchange. Explain which compound operations are missing and how to perform an additional operation if needed.
- Atomic update return values. Distinguish the values returned by named fetch operations, compound assignment, and pre- versus post-increment or decrement on atomic integral types.
- User-defined atomic type requirements. State that std::atomic<UDT> offers load, store, exchange, compare-exchange, assignment, and conversion, but requires a type with trivial copy assignment, including its bases and non-static members. Describe why a type such as std::vector<int> does not qualify, while a simple class of counters, flags, or pointers may.
- Representation-based compare-exchange results. Explain that compare-exchange compares the stored representations bitwise, as if with memcmp, rather than calling a type’s equality operator. Describe how padding or different representations of equal values can make even compare_exchange_strong fail, including for atomic floating-point values.
- Restrictions on user code. Justify the restrictions on user-defined atomic types: implementations may protect operations with an internal lock, so calling user-written assignment or comparison code on protected data could expose it, block other threads for too long, or cause deadlock. Explain that treating the type as raw bytes can also enable direct atomic instructions.
- Atomic free-function forms. Explain how atomic free functions correspond to member operations: they take a pointer to the atomic object, usually add an atomic_ prefix, and use _explicit variants when specifying memory orders.
- Atomic access to shared pointers. Explain that atomic free functions provide load, store, exchange, and compare-exchange for std::shared_ptr, even though std::shared_ptr is not itself an atomic type; concurrent access to the same shared_ptr object needs these functions on all threads or other synchronization.
- Ready flag ordering chain. Trace how a writer populates data before setting an atomic ready flag, while a reader waits for the flag before reading the data. Explain how the flag write synchronizes with the read that sees true, making the data write happen before the data read.
- Atomic write-read synchronization. Explain that a suitably tagged atomic write on a variable synchronizes with a suitably tagged atomic read of that variable when the read observes the written value, a later value written by the same thread, or a value carried through a sequence of atomic read-modify-write operations. State that this relationship fundamentally comes from atomic operations, even when exposed through an operation such as locking a mutex.
- Single-thread operation ordering. Explain when operations in one thread have a happens-before relationship: an operation sequenced before another happens before it, but operations within one statement are generally unordered unless a sequencing rule applies.
- Cross-thread ordering chain. Trace how a synchronizes-with relationship establishes inter-thread happens-before, and how transitivity and sequenced-before relationships extend that ordering to surrounding operations in both threads.
- Sequentially consistent global order. Explain that when threads use sequentially consistent atomic operations, those operations fit one total order seen consistently by all such threads. Apply that order to the x-and-y example to explain why its final assertion cannot find both reads false.
- Relaxed per-variable visibility. Explain that relaxed atomic operations do not create synchronizes-with relationships: each atomic variable retains its own modification order, but threads need not agree on the relative order of changes to different variables. Describe why seeing relaxed y become true does not guarantee seeing an earlier relaxed write to x.
- Acquire-release synchronization pair. Explain that a release store synchronizes with an acquire load that reads its value, carrying earlier operations in the releasing thread before later operations in the acquiring thread. Apply this to the x-then-y example: making y the release/acquire handoff orders x even if x itself uses relaxed operations.
- Acquire-release without total order. Distinguish acquire-release ordering from sequential consistency: it establishes synchronization through matching release/acquire operations but not one total order across all threads. Explain why the independent x and y writers and their readers can still produce two false reads in the acquire-release version of the example.
- Release sequence synchronization rules. Explain when a store and a later load are connected by a release sequence, including the permitted memory orders and the requirement that each intervening read-modify-write operation reads the preceding value.
- Shared queue consumer visibility. Trace how a producer's release store to an atomic queue count makes buffer writes visible to multiple consumers whose acquire fetch_sub operations claim items. Explain why the second consumer is safe even though it reads the first consumer's count update rather than the producer's store.
- Release acquire fence synchronization. Trace how a release fence before a relaxed store to y and an acquire fence after a relaxed load of y synchronize when that load reads the store’s value. Explain why this makes the earlier store to x happen before the later load of x, so the assertion cannot fail.
- Synchronization at fence boundaries. Explain that the fence itself is the synchronization point: moving the store to x after the release fence removes the ordering guarantee for x, even if that store still precedes the store to y.
- Fence ordering of non-atomic data. Explain why x can be an ordinary non-atomic bool in the example: fences order the write to x before the atomic write to y and the atomic read of y before the read of x, establishing a happens-before relationship when the reader sees the stored value of y.
- Ordering of non-atomic data. Explain how sequencing a non-atomic operation before a synchronizing operation can make that non-atomic operation happen before an operation in another thread.
- Spinlock release and acquisition. Trace how a spinlock's release clear and subsequent acquire test-and-set order accesses to the data it protects.
- Condition variable synchronization source. Explain why a condition variable itself supplies no synchronizes-with relationship and where the synchronization in a condition-variable wait comes from.
What it builds on
- Using mutexes in C++ (chapter 3) · Introduces mutexes for protecting shared data.
- Waiting for a condition with condition variables (chapter 4) · Introduces condition variables for signaling and waiting.
- Race conditions (chapter 3) · The earlier discussion of race conditions.
- Structuring code for protecting shared data (chapter 3) · The guideline against passing references to protected data into user-supplied functions.
- Launching a thread (chapter 2) · Launching a std::thread
- Waiting for a thread to complete (chapter 2) · Joining a thread
- Waiting for one-off events with futures (chapter 4) · Futures, promises, and background tasks
- Continuation-style concurrency with the Concurrency TS (chapter 4) · Continuations
Section by section
Introduction
This introduction explains why C++ needs a multithreading-aware memory model, when programmers need its details, and how the chapter will approach atomic synchronization.
- Memory model foundation: Explain that the C++ memory model defines how the building blocks of multithreading work, making higher-level facilities reliable, while programmers using mutexes and event-signaling facilities usually need not study its details.
Why it matters: It explains why familiar synchronization tools work without requiring every user to reason about the memory model directly. - Atomic synchronization and chapter path: Describe atomic types and operations as C++ facilities for low-level, close-to-the-machine synchronization, often implemented with one or two CPU instructions. State that the chapter moves from memory-model basics to atomic types and operations, then to synchronization using them; these details chiefly matter when writing code that uses atomics for synchronization, such as lock-free data structures.
Why it matters: It helps the reader judge when the chapter’s detailed treatment of atomics is needed.
Key terms: multithreading-aware memory model, mutexes, condition variables, futures, latches, barriers, atomic types, atomic operations, synchronization, lock-free data structures
5.1 Memory model basics
Introduces the structural and concurrency aspects of the C++ memory model and explains why the discussion begins with objects and memory locations.
- Memory model starting point: Distinguish the memory model’s structural aspects, concerning how things are laid out in memory, from its concurrency aspects. Explain that the section starts with C++ objects and memory locations because structure matters to concurrency, especially low-level atomic operations.
Why it matters: This gives you a reason to study memory layout before considering concurrent operations.
Key terms: memory model, structural aspects, concurrency aspects, objects, memory locations, low-level atomic operations
5.1.1 Objects and memory locations
This section defines C++ objects and memory locations, then shows how ordinary members and bit fields divide a struct’s storage.
- Objects and memory locations: Explain that a C++ object is a region of storage with a type and lifetime, and that an object may contain sub-objects and occupy multiple memory locations. State that each scalar object occupies one memory location, even when adjacent to other scalars or in an array.
Why it matters: Memory locations provide the unit for reasoning about which data threads access. - Bit-field memory location boundaries: Describe how adjacent bit fields are distinct objects that share one memory location, while a zero-length bit field separates the following bit field into another memory location without occupying one itself.
Why it matters: Bit fields show why counting objects is not enough to identify separate memory locations.
Key terms: object, sub-object, memory location, scalar type, adjacent bit fields, zero-length bit field
5.1.2 Objects, memory locations, and concurrency
The section explains when concurrent access to a memory location becomes a data race and how ordering or atomic access keeps behavior defined.
- Shared memory access conditions: Distinguish harmless concurrent accesses from potentially conflicting ones: separate memory locations can be accessed independently, and shared read-only access needs no protection, but a write to a location accessed by another thread requires care.
Why it matters: This tells you which accesses need closer scrutiny when threads share data. - Enforced ordering between accesses: Explain that conflicting accesses need an enforced ordering, which may differ between runs, and that locking the same mutex or using the synchronization properties of atomic operations can establish it. State that when more than two threads access a location, each pair of accesses needs a defined ordering.
Why it matters: An ordering makes conflicting accesses safe without requiring you to predict which thread goes first. - Data races and atomic access: State the section's conditions for a data race: unordered accesses from separate threads to one memory location, with at least one write and at least one non-atomic access, cause undefined behavior. Distinguish this from atomic accesses whose order of arrival remains unspecified but whose behavior is defined.
Why it matters: This distinction explains both why a data race is a serious bug and what atomic access can—and cannot—guarantee.
Key terms: memory location, read-only data, race condition, enforced ordering, mutexes, synchronization properties of atomic operations, data race, undefined behavior, atomic operations
5.1.3 Modification orders
This section defines each object’s modification order and explains the agreement and ordering constraints it imposes on threads.
- Per-object modification order: Explain that an object’s modification order contains its initialization and every write to it from all threads, and that all threads must agree on that order in a given execution. Distinguish this per-object agreement from any required relative order between operations on separate objects.
Why it matters: A shared order for each object does not, by itself, establish an order across different objects. - Synchronization responsibility for shared objects: Explain that programmers must provide sufficient synchronization for non-atomic objects so threads agree on their modification orders; incompatible sequences for one variable constitute a data race and undefined behavior. State that atomic operations instead place the necessary synchronization responsibility on the compiler.
Why it matters: Without the required agreement, accesses to a non-atomic shared variable can have undefined behavior. - Later-value observation constraints: Describe the constraints imposed once a thread has seen a value in an object’s modification order: its later reads cannot return earlier values, and its later writes to that object must occur later in the order. Explain that a read following a write to the same object in one thread must return that write’s value or a later value.
Why it matters: These constraints rule out speculative behavior that would make a thread move backward through an object’s modification order.
Key terms: modification order, initialization, object, non-atomic, atomic operations, data race, undefined behavior, synchronization, speculative execution, subsequent reads
5.2 Atomic operations and types in C++
This section introduces atomic operations as indivisible, contrasts them with potentially mixed observations of non-atomic operations, and explains why C++ generally uses atomic types for atomic operations.
- Atomic operations and reads: Explain that an atomic operation cannot be observed half-done and that an atomic load, when all modifications to the object are atomic, reads either its initial value or a value stored by a modification. State that C++ generally requires an atomic type to obtain an atomic operation.
Why it matters: This distinction is the starting point for understanding what C++ atomic types guarantee. - Non-atomic observations and data races: Describe how another thread may observe a non-atomic operation only partly completed, including a mixture of values when an operation consists of multiple atomic operations. Explain that unsynchronized access to non-atomic variables can constitute a data race and cause undefined behavior.
Why it matters: Individually atomic parts do not make a larger operation atomic, and unsynchronized non-atomic access can invalidate the program.
Key terms: atomic operation, atomic load, atomic type, non-atomic operation, data race, undefined behavior
5.2.1 The standard atomic types
This section introduces the standard atomic types, how to check whether they are lock-free, their available operations, and the memory-ordering arguments those operations accept.
- Lock-free status of atomic types: Explain that standard atomic operations are atomic by the language definition but may use an internal lock, and describe how to check whether a type is lock-free.
Why it matters: An atomic type that uses an internal lock may not deliver the performance benefit you expected over a mutex. - Atomic flag special status: Describe why std::atomic_flag is exceptional and what operations its deliberately limited interface provides.
Why it matters: The guaranteed lock-free flag provides a basis for building a simple lock and other atomic implementations. - Atomic type names and aliases: Describe how std::atomic<T> and the alternative atomic type names relate, including the portability caution about mixing them.
Why it matters: Using std::atomic<T> directly avoids a portability trap with older compilers. - Atomic operation interfaces and returns: Describe the common atomic operations, how available operations depend on the underlying type, and why their return values are values rather than references.
Why it matters: The interface and return-value rules determine what you can do atomically without introducing a separate read. - Memory-order choices by operation: Distinguish the memory-order values permitted for stores, loads, and read-modify-write operations, and state the default when no order is supplied.
Why it matters: The allowed ordering arguments are the starting point for choosing synchronization semantics in the next section.
Key terms: <atomic>, is_lock_free(), is_always_lock_free, ATOMIC_*_LOCK_FREE, std::atomic_flag, test_and_set(), clear(), std::atomic<T>, atomic_ prefix, atomic typedefs
5.2.2 Operations on std::atomic_flag
This section introduces std::atomic_flag’s initialization and operations, then uses it to build a basic spin-lock mutex while explaining its limitations.
- Flag initialization and guarantees: Describe how ATOMIC_FLAG_INIT initializes an std::atomic_flag to the clear state, regardless of its scope. State that it is the only standard atomic type guaranteed to be lock-free and that static-storage flags avoid initialization-order issues.
Why it matters: The flag must start in a known state before threads use it. - Flag operations and ordering: Explain that clear() clears the flag and test_and_set() sets it while returning its previous value. Distinguish their memory-order options: clear() is a store that cannot use acquire or acq_rel, test_and_set() is a read-modify-write operation that can use any ordering tag, and both default to seq_cst.
Why it matters: These two operations determine both how the flag changes and what ordering a caller may request. - Atomic object copying restriction: Justify why an std::atomic_flag cannot be copy-constructed or copy-assigned from another flag, and note that the restriction applies to atomic types generally: reading one object and writing another are separate operations whose combination cannot be atomic.
Why it matters: The restriction follows from what an atomic operation can guarantee, rather than from a quirk of this flag. - Spin-lock use and limits: Trace how a spin-lock mutex starts with a clear flag, repeatedly calls test_and_set() until its previous value was false to lock, and calls clear() to unlock. Explain that it provides mutual exclusion and can work with std::lock_guard<>, but busy-waits under contention; its lack of a nonmodifying query also makes std::atomic<bool> better for a general Boolean flag.
Why it matters: The example shows a practical use for the minimal flag API and why it is not a general-purpose Boolean or an ideal contended mutex.
Key terms: std::atomic_flag, ATOMIC_FLAG_INIT, clear state, lock-free, static storage duration, clear(), test_and_set(), read-modify-write, memory_order_acquire, memory_order_acq_rel
5.2.3 Operations on std::atomic<bool>
This section introduces the operations on std::atomic<bool>, especially compare-exchange, and explains how retries, memory ordering, and lock-free support affect their use.
- Atomic Boolean operations: Describe how std::atomic<bool> can be initialized and updated from a plain bool, and distinguish store(), load(), and exchange() from the more limited operations on std::atomic_flag.
Why it matters: These operations are the starting point for using atomic values without a separate mutex. - Boolean assignment result: Explain why assigning a plain bool to std::atomic<bool> returns a plain bool holding the assigned value, rather than a reference to the atomic object.
Why it matters: The return value tells you what was stored without requiring a second atomic read that another thread might affect. - Compare-exchange success and failure: Trace what compare_exchange_weak() or compare_exchange_strong() does when the atomic value matches—or does not match—the supplied expected value.
Why it matters: Compare-exchange is the basic conditional update used when programming with atomic types. - Weak and strong retry choices: Distinguish spurious failure in compare_exchange_weak() from failure in compare_exchange_strong(), and explain how that affects retry loops.
Why it matters: A retry strategy must account for whether failure actually means another value was present. - Compare-exchange ordering and lock freedom: Explain why compare-exchange may specify separate success and failure memory orders, and state the failure-order restrictions and defaults. State how to check whether std::atomic<bool> operations are lock-free.
Why it matters: A failed conditional update has no store, and atomicity does not by itself promise a lock-free implementation.
Key terms: std::atomic<bool>, std::atomic_flag, store(), load(), exchange(), assignment operator, non-atomic bool, compare-exchange, expected, desired
5.2.4 Operations on std::atomic<T*>: pointer arithmetic
This section explains the interface and pointer arithmetic of std::atomic<T*>, including return values and memory-ordering options.
- Atomic pointer interface: Describe std::atomic<T*> as an atomic pointer that supports load, store, exchange, and compare-exchange operations on T* values, but cannot be copied or copy-assigned.
Why it matters: It establishes which familiar atomic operations also apply to pointers. - Atomic pointer arithmetic returns: Explain how fetch_add(), fetch_sub(), and the arithmetic operators move an atomic pointer by elements, and distinguish the pointer returned by fetch_add() from the pointer returned by +=.
Why it matters: The returned pointer determines whether subsequent code acts on the old position or the new one. - Pointer arithmetic memory ordering: Distinguish the memory-ordering options for pointer arithmetic function calls from those for operator forms, and state why fetch_add() and fetch_sub() can participate in a release sequence.
Why it matters: Choosing a function rather than an operator matters when an atomic pointer update needs an explicit memory order.
Key terms: std::atomic<T*>, load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong(), fetch_add(), fetch_sub(), exchange-and-add, +=
5.2.5 Operations on standard atomic integral types
This section describes the extra operations available on standard atomic integral types and distinguishes which value each form returns.
- Atomic integral operation set: Describe the arithmetic and bitwise operations available on atomic integral types beyond load, store, exchange, and compare-exchange. Explain which compound operations are missing and how to perform an additional operation if needed.
Why it matters: These operations cover common counter and bitmask uses without requiring a custom update loop. - Atomic update return values: Distinguish the values returned by named fetch operations, compound assignment, and pre- versus post-increment or decrement on atomic integral types.
Why it matters: Choosing the right operation depends on whether the code needs the value before or after the update.
Key terms: atomic integral types, fetch_add(), fetch_sub(), fetch_and(), fetch_or(), fetch_xor(), compound-assignment operators, pre-increment, post-increment
5.2.6 The std::atomic<> primary class template
Explains which types can use the std::atomic<> primary template, what operations it offers, and why representation and implementation choices matter.
- User-defined atomic type requirements: State that std::atomic<UDT> offers load, store, exchange, compare-exchange, assignment, and conversion, but requires a type with trivial copy assignment, including its bases and non-static members. Describe why a type such as std::vector<int> does not qualify, while a simple class of counters, flags, or pointers may.
Why it matters: These requirements determine whether a user-defined type can be used with the primary atomic template at all. - Representation-based compare-exchange results: Explain that compare-exchange compares the stored representations bitwise, as if with memcmp, rather than calling a type’s equality operator. Describe how padding or different representations of equal values can make even compare_exchange_strong fail, including for atomic floating-point values.
Why it matters: Value equality alone does not guarantee that a compare-exchange operation will succeed. - Restrictions on user code: Justify the restrictions on user-defined atomic types: implementations may protect operations with an internal lock, so calling user-written assignment or comparison code on protected data could expose it, block other threads for too long, or cause deadlock. Explain that treating the type as raw bytes can also enable direct atomic instructions.
Why it matters: The restrictions protect both the safety and potential efficiency of atomic operations. - Atomic type suitability and hardware: Describe why small eligible types are more likely to use atomic instructions, while some platforms also support types up to twice a machine word through double-word compare-and-swap. Distinguish these simple assignment-and-comparison uses from complex structures that need other operations and are better protected with a mutex; note that floating-point atomics do not provide atomic arithmetic here.
Why it matters: Choosing between an atomic object and a mutex depends on the operations needed, not just whether the type can be instantiated.
Key terms: std::atomic<UDT>, primary template, trivial copy-assignment operator, load(), store(), exchange(), bitwise comparison, memcmp, compare_exchange_strong, compare_exchange_weak
5.2.7 Free functions for atomic operations
This section explains the free-function forms of atomic operations, their use with shared pointers, and why the atomic shared-pointer type can be a safer choice.
- Atomic free-function forms: Explain how atomic free functions correspond to member operations: they take a pointer to the atomic object, usually add an atomic_ prefix, and use _explicit variants when specifying memory orders.
Why it matters: These forms let you recognize the same atomic operation whether it is written as a member call or a free-function call. - Free-function exceptions and arguments: Distinguish the less uniform free-function signatures: compare-exchange takes a pointer to the expected value and its _explicit form requires both success and failure memory orders; atomic_flag operations include flag in their names.
Why it matters: These differences matter when translating member calls into free-function calls. - Atomic access to shared pointers: Explain that atomic free functions provide load, store, exchange, and compare-exchange for std::shared_ptr, even though std::shared_ptr is not itself an atomic type; concurrent access to the same shared_ptr object needs these functions on all threads or other synchronization.
Why it matters: Mixing atomic and unprotected access to one shared_ptr object can still cause a data race. - Atomic shared-pointer type choice: Describe std::experimental::atomic_shared_ptr<T> as a Concurrency TS atomic type with load, store, exchange, and compare-exchange; explain why the section recommends it over atomic free functions on a plain shared_ptr, while still checking lock-freedom and profiling performance claims.
Why it matters: Making atomic access part of the type helps prevent accidental non-atomic access without promising a speedup.
Key terms: free functions, atomic_, _explicit, memory-ordering tag, std::atomic_compare_exchange_weak_explicit, success and failure memory orders, std::atomic_flag_test_and_set, std::atomic_flag_clear, std::shared_ptr, atomic load
5.3 Synchronizing operations and enforcing ordering
This section uses an atomic ready flag to show how synchronization can order non-atomic data accesses across threads.
- Ready flag ordering chain: Trace how a writer populates data before setting an atomic ready flag, while a reader waits for the flag before reading the data. Explain how the flag write synchronizes with the read that sees true, making the data write happen before the data read.
Why it matters: The ordering chain explains why the shared data itself need not be atomic in this example. - Default atomic ordering qualification: Explain that the illustrated ordering holds with the default atomic operations, but atomic operations have other ordering options whose requirements may differ.
Why it matters: This distinction prevents treating the example's ordering as a promise made by every atomic operation.
Key terms: std::atomic<bool>, data_ready, synchronizes-with, happens-before, non-atomic, default atomic operations, ordering requirements
5.3.1 The synchronizes-with relationship
Defines when an atomic write synchronizes with an atomic read and explains that memory-order tagging determines whether the relationship applies.
- Atomic write-read synchronization: Explain that a suitably tagged atomic write on a variable synchronizes with a suitably tagged atomic read of that variable when the read observes the written value, a later value written by the same thread, or a value carried through a sequence of atomic read-modify-write operations. State that this relationship fundamentally comes from atomic operations, even when exposed through an operation such as locking a mutex.
Why it matters: This relationship is a building block for reasoning about which operations in different threads are ordered. - Memory-order tagging qualification: Explain that atomic operations are suitably tagged by default for the simple store-and-load example, but that C++ memory-order constraints determine when a synchronizes-with relationship applies.
Why it matters: The simple store-and-load rule needs qualification when explicit memory ordering is used.
Key terms: synchronizes-with relationship, atomic write, atomic read, atomic read-modify-write operations, fetch_add(), compare_exchange_weak(), suitably-tagged, memory ordering, ordering constraints
5.3.2 The happens-before relationship
This section explains how sequencing within a thread and synchronization between threads establish happens-before ordering, and where strongly-happens-before differs.
- Single-thread operation ordering: Explain when operations in one thread have a happens-before relationship: an operation sequenced before another happens before it, but operations within one statement are generally unordered unless a sequencing rule applies.
Why it matters: Source-code order does not always determine the order in which expressions are evaluated. - Cross-thread ordering chain: Trace how a synchronizes-with relationship establishes inter-thread happens-before, and how transitivity and sequenced-before relationships extend that ordering to surrounding operations in both threads.
Why it matters: One synchronization point can make a series of earlier changes visible to later operations in another thread. - Strong happens-before distinction: Distinguish strongly-happens-before from happens-before: both follow sequenced-before and synchronizes-with relationships and are transitive, but memory_order_consume can contribute to inter-thread happens-before without contributing to strongly-happens-before.
Why it matters: The distinction explains why the book normally says “happens-before” while reserving an exception for consume ordering.
Key terms: sequenced-before, happens-before, strongly-happens-before, synchronizes-with, inter-thread happens-before, transitive, memory_order_consume
5.3.3 Memory ordering for atomic operations
This section compares sequentially consistent, relaxed, and acquire-release atomic ordering by showing which observations and synchronization each permits.
- Memory ordering choices and costs: Describe the six memory-order options as three models: sequential consistency, relaxed ordering, and acquire-release ordering. Explain why the easier-to-reason-about sequentially consistent default may cost more on some processor architectures.
Why it matters: The ordering choice determines both which results your code permits and what synchronization it may require. - Sequentially consistent global order: Explain that when threads use sequentially consistent atomic operations, those operations fit one total order seen consistently by all such threads. Apply that order to the x-and-y example to explain why its final assertion cannot find both reads false.
Why it matters: A single shared order makes possible outcomes much easier to check. - Relaxed per-variable visibility: Explain that relaxed atomic operations do not create synchronizes-with relationships: each atomic variable retains its own modification order, but threads need not agree on the relative order of changes to different variables. Describe why seeing relaxed y become true does not guarantee seeing an earlier relaxed write to x.
Why it matters: Relaxed atomics prevent unsynchronized atomic access but do not, by themselves, publish other writes to a reader. - Acquire-release synchronization pair: Explain that a release store synchronizes with an acquire load that reads its value, carrying earlier operations in the releasing thread before later operations in the acquiring thread. Apply this to the x-then-y example: making y the release/acquire handoff orders x even if x itself uses relaxed operations.
Why it matters: The matching handoff supplies targeted cross-thread ordering without requiring a total order of all atomic operations. - Acquire-release without total order: Distinguish acquire-release ordering from sequential consistency: it establishes synchronization through matching release/acquire operations but not one total order across all threads. Explain why the independent x and y writers and their readers can still produce two false reads in the acquire-release version of the example.
Why it matters: Acquire-release does not justify treating independently published values as though every thread sees one global sequence.
Key terms: memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst, single total ordering, sequential consistency, synchronizes-with, modification order
5.3.4 Release sequences and synchronizes-with
This section explains how a release sequence lets an atomic store synchronize with later loads through intervening read-modify-write operations, and why that matters for consumers of a shared queue.
- Release sequence synchronization rules: Explain when a store and a later load are connected by a release sequence, including the permitted memory orders and the requirement that each intervening read-modify-write operation reads the preceding value.
Why it matters: These rules determine whether a thread can safely use data published before an atomic store. - Shared queue consumer visibility: Trace how a producer's release store to an atomic queue count makes buffer writes visible to multiple consumers whose acquire fetch_sub operations claim items. Explain why the second consumer is safe even though it reads the first consumer's count update rather than the producer's store.
Why it matters: The queue example shows why release sequences avoid extra consumer-to-consumer synchronization without losing safe access to published data.
Key terms: release sequence, synchronizes-with, dependency-ordered-before, read-modify-write, memory_order_release, memory_order_acquire, atomic<int>, count.store, fetch_sub, shared buffer
5.3.5 Fences
This section shows how release and acquire fences can order relaxed atomic operations, why fence placement matters, and how that ordering can ultimately protect non-atomic data.
- Fence ordering without modification: Explain that fences impose memory-ordering constraints without modifying data: they restrict how operations in the executing thread can be reordered, often alongside relaxed atomic operations.
Why it matters: This distinguishes a fence from an atomic load or store and explains why it can change the outcome without changing a value itself. - Release acquire fence synchronization: Trace how a release fence before a relaxed store to y and an acquire fence after a relaxed load of y synchronize when that load reads the store’s value. Explain why this makes the earlier store to x happen before the later load of x, so the assertion cannot fail.
Why it matters: The value observed through an atomic operation determines whether fences establish the cross-thread ordering the program needs. - Synchronization at fence boundaries: Explain that the fence itself is the synchronization point: moving the store to x after the release fence removes the ordering guarantee for x, even if that store still precedes the store to y.
Why it matters: Correct placement, not merely the presence of a fence or source-code order, determines which operations it orders. - Ordering non-atomic data: State that the broader benefit of atomic ordering is that it can order non-atomic operations across threads and thereby avoid the undefined behavior of a data race.
Why it matters: It connects the all-atomic fence example to the practical reason for establishing cross-thread ordering.
Key terms: fence, memory barrier, memory_order_relaxed, release fence, acquire fence, synchronizes-with, happens-before, synchronization point, non-atomic operations, data race
5.3.6 Ordering non-atomic operations with atomics
This section shows how atomic operations and fences can order accesses to non-atomic data across threads without a data race.
- Fence ordering of non-atomic data: Explain why x can be an ordinary non-atomic bool in the example: fences order the write to x before the atomic write to y and the atomic read of y before the read of x, establishing a happens-before relationship when the reader sees the stored value of y.
Why it matters: It shows how threads can safely communicate ordinary data by ordering access to it. - Other ordering mechanisms: Describe that fences are not the only way to order non-atomic accesses: a memory_order_release/memory_order_consume pair can do so, as in the earlier dynamically allocated object example.
Why it matters: It keeps the reader from treating fences as the only tool for safely ordering ordinary data.
Key terms: fences, non-atomic bool, happens-before, data race, atomic, memory_order_release, memory_order_consume, non-atomic accesses, memory_order_relaxed
5.3.7 Ordering non-atomic operations
This section shows how synchronization operations establish happens-before ordering for non-atomic data, then identifies the guarantees provided by C++ concurrency facilities.
- Ordering of non-atomic data: Explain how sequencing a non-atomic operation before a synchronizing operation can make that non-atomic operation happen before an operation in another thread.
Why it matters: This is the reason synchronization can protect ordinary, non-atomic data. - Spinlock release and acquisition: Trace how a spinlock's release clear and subsequent acquire test-and-set order accesses to the data it protects.
Why it matters: The spinlock makes the cross-thread ordering chain concrete. - Thread and mutex synchronization: Describe the synchronization points for starting and joining a std::thread and for unlocking and subsequently acquiring the same mutex, including shared mutexes.
Why it matters: These guarantees explain where familiar thread and lock operations establish cross-thread ordering. - Future readiness synchronization: Describe how completing a promise, packaged task, or async task synchronizes with a successful wait or get on a future sharing its state, and how readiness orders continuations.
Why it matters: Future readiness is a synchronization event, not just a way to retrieve a result. - Latch and barrier ordering: Distinguish the synchronization supplied by experimental latches and barriers, including the flex barrier's completion function.
Why it matters: These facilities also order work on opposite sides of a synchronization point. - Condition variable synchronization source: Explain why a condition variable itself supplies no synchronizes-with relationship and where the synchronization in a condition-variable wait comes from.
Why it matters: It prevents attributing data-ordering guarantees to a notification rather than to the associated mutex.
Key terms: non-atomic operation, sequenced before, synchronizes-with, happens-before, std::atomic_flag, test_and_set, clear, std::memory_order_acquire, std::memory_order_release, lock
5.4 Summary
The chapter recap connects C++ atomic types, memory ordering, and fences to cross-thread synchronization, then points toward their use in concurrent containers and parallel algorithms.
Where this is used later
- Applying the memory model to the lock-free stack (chapter 7) · Applies the memory model to a lock-free stack.
- (chapter intro) (chapter 6) · Introduces atomic operations used in the later lock-free chapter.
- Lock-free data structures (chapter 7) · Evaluating whether a data structure's atomic operations are actually lock-free.
- std::execution::parallel_policy (chapter 10) · Introduces standard atomic types as an alternative for shared counters.
- Types of nonblocking data structures (chapter 7) · The use of std::atomic_flag and test_and_set().
- Writing a thread-safe stack without locks (chapter 7) · A lock-free stack uses conditional atomic updates.
- Data contention and cache ping-pong (chapter 8) · Atomic read-modify-write operations on integral types.
- Detecting nodes in use with reference counting (chapter 7) · The std::atomic<> template can operate on a suitable struct.
Concepts explained in this chapter
- synchronizes-with relationA C++ memory-model relation between operations that establishes inter-thread ordering and visibility.
- memory orderingThe constraints governing how memory operations are ordered and observed across threads in the C++ memory model.
- modification orderA per-object order of all writes to that object, including its initialization, on which all threads in a given execution must agree.
- acquire-release semanticsAcquire-release ordering provides synchronization when an acquire operation reads a value written by a release operation, without imposing a single global order.
- sequential consistencySequentially consistent ordering requires atomic operations using that ordering to fit one total order consistent with each thread's program order.
- acquire semanticsAn acquire operation is an atomic operation whose ordering can synchronize with a release operation on the same memory location.
- lockless techniqueA progress guarantee under which, when threads continue taking steps, at least one operation completes despite contention.
- happens-before relationA C++ memory-model ordering relation between evaluations that establishes when one precedes another, including across threads.
- generic atomic interfaceA C++ standard library class template whose specializations provide atomic operations on values of supported types.
- atomic typeAn atomic type is a type whose operations provide atomic access to an object and support synchronization between threads.
- sequenced-before relationA C++ ordering relation between evaluations within a single thread.
- atomic_flagA C++ standard library atomic Boolean flag with test-and-set and clear operations.
- std::atomic<bool>A C++ standard library atomic type that provides atomic operations on a Boolean value.
- release operationA release operation is an atomic operation whose ordering can synchronize with an acquire operation on the same memory location.
- std::memory_order`std::memory_order` is a C++ standard library enumeration whose values specify memory-ordering constraints for atomic operations.
- inter-thread orderingThe inter-thread happens-before relationship orders operations across threads through synchronization and sequencing rules.
- compare-and-exchange operationAn operation that atomically compares an object's value with an expected value and conditionally replaces it with a desired value.
- memory barrierA fence is a synchronization operation that constrains the ordering of memory operations without itself accessing the protected object.
- memory locationA memory location in C++ is a storage unit consisting of an object of scalar type or a sequence of adjacent bit-fields.
- bitwise comparisonA bitwise comparison determines equality by comparing the object representations of values rather than applying their value-level comparison operators.
- atomic operationAn atomic operation is an operation that appears indivisible to other threads under the C++ memory model.
- release sequenceA release sequence is a sequence of operations on an atomic object beginning with a release operation and continuing through qualifying read-modify-write operations that read the preceding value.
- data raceA data race occurs when concurrent operations access the same memory location, at least one operation modifies it, and the operations are not properly synchronized.
- undefined behaviorUndefined behavior is program behavior for which the C++ language standard imposes no requirements.
- thread-aware memory modelA programming-language memory model that defines how operations on memory behave and become visible across threads.
- std::memory_order_releaseA C++ memory-order value that gives an atomic store release semantics, making preceding operations visible to a thread that performs a matching acquire operation.
- is_lock_free()`is_lock_free()` is an atomic member function that reports whether operations on an atomic object are lock-free.
- spinlockA mutex implementation in which a waiting thread repeatedly attempts to acquire the lock rather than blocking.
- clear()`clear()` is a `std::atomic_flag` member function that sets the flag to the clear state.
- test-and-set`test_and_set()` is a `std::atomic_flag` member function that sets the flag and returns its previous value as one atomic operation.
- read-modify-write operationA read-modify-write operation atomically reads a value, modifies it, and writes the result as one operation.
- std::experimental::atomic_shared_ptr`std::experimental::atomic_shared_ptr` is a C++ Concurrency TS type that provides atomic operations on shared-pointer ownership.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
