Chapter 3: Chapter 3. Sharing data between threads
The chapter introduces the benefits and risks of sharing data between threads and previews ways to protect it.
After this chapter you will be able to
- Rules for shared data. Explain why easy data sharing is both a benefit and a source of concurrency bugs: threads need rules for when each can access data and how updates are communicated to other threads.
- Temporary linked-list invariant break. Trace removal of a node from a doubly linked list and explain why its reciprocal next and previous links cannot both remain consistent throughout the update.
- Consequences of observing partial updates. Explain how a second thread can encounter a partly removed list node and why the result may range from skipping that node to corrupting the list; identify this as an example of a race condition.
- Race conditions and invariants. Explain that a race condition occurs when the outcome depends on the relative order of operations in two or more threads, and distinguish acceptable outcomes from races that break a system invariant.
- Partially completed shared updates. Describe how an operation that updates separate pieces of shared data can expose a partially completed state to another thread. Explain why the short timing window makes the failure intermittent and why increased activity or a debugger can change whether it appears.
- Protected intermediate states. Explain how protecting a shared data structure prevents other threads from seeing intermediate states that break its invariants, so a modification appears not to have started or to have completed.
- Mutex protection of shared data. Explain how locking a mutex associated with shared data makes access mutually exclusive and prevents other threads from observing the data structure partway through a modification.
- RAII mutex locking. Explain how std::lock_guard<std::mutex> locks a mutex when constructed and unlocks it when destroyed, including when a function exits through an exception.
- Shared list mutual exclusion. Describe how the list example uses one mutex and lock guards in both add_to_list() and list_contains() so a read cannot occur partway through a modification.
- Protected data interface boundaries. Explain why the mutex and data are usually grouped as private members of a class, and how returning a pointer or reference to the data defeats the protection.
- Protected data escaping the lock. Explain why locking a member function is insufficient if a pointer or reference to its protected data escapes. Describe how to prevent access outside the lock’s protection.
- Whole-operation data protection. Explain why a mutex must protect all shared data involved in a complete operation, not merely each individual access. In the linked-list deletion example, that means protecting the node being removed and its neighbors throughout the deletion.
- Stale stack state checks. Explain why a mutex-protected stack's empty() and size() results cannot guarantee its state when the caller acts on them. Another thread can remove the last item between empty() and top(), making top() on an empty stack undefined behavior.
- Retrieval and removal conflict. Trace how separate top() and pop() calls let two threads read the same item, process it twice, and discard another item unread. Explain why combining retrieval and removal fixes that race but can lose an item if copying a value for return throws after removal.
- Safe pop interface choices. Describe the proposed ways to combine retrieval and removal without ignoring exception safety: pop() into a caller-supplied reference, restrict value returns to no-throw copyable or movable types, or return a pointer such as std::shared_ptr. State a significant cost or restriction of each and why an interface may offer more than one form.
- Deadlock and lock ordering. Explain how two threads can deadlock when each holds one mutex and waits for the other, and why a consistent lock order prevents that cycle. Describe why ordering mutexes by argument position fails when two threads swap the same objects with reversed arguments.
- Joint locking and ownership. Describe the swap example's check that the objects differ, its use of std::lock to acquire both mutexes without deadlock, and its use of std::lock_guard with std::adopt_lock to release the already-acquired locks on exit.
- Scoped lock alternative and limits. Describe how C++17 std::scoped_lock acquires multiple mutexes with the deadlock-avoiding algorithm and releases them on destruction, usually replacing the separate std::lock and guard steps. State that neither facility prevents deadlock when locks are acquired separately.
- Deadlock as a wait cycle. Explain that deadlock occurs when threads form a cycle of waiting for one another, even without mutexes, as when each thread tries to join the other. State the guiding rule: do not wait for another thread if it might be waiting for you.
- Single-lock and callback precautions. Explain why avoiding nested locks removes deadlock caused solely by mutex acquisition, and why calling user-supplied code while holding a lock can silently reintroduce nesting. Describe std::lock as the option for acquiring multiple locks as one deadlock-avoiding operation.
- Consistent lock acquisition order. Explain that when multiple locks cannot be acquired together with std::lock, every thread should acquire them in a consistent order. Use hand-over-hand list traversal or deletion to show how opposing acquisition orders can create a deadlock.
- Runtime-checked lock hierarchy. Describe a lock hierarchy that assigns mutexes levels and permits a thread holding one hierarchical mutex to acquire only a lower-level one. Explain that this detects ordering violations at runtime, while equal-level simultaneous locking is also disallowed.
- Deferred locking with unique_lock. Describe how std::defer_lock constructs a std::unique_lock without locking its mutex, and how the lock can later be acquired through the unique_lock or by passing it to std::lock(). Distinguish this from std::adopt_lock, which manages a mutex that is already locked.
- Tracked mutex ownership. Explain that std::unique_lock tracks whether it currently owns its associated mutex, updates that state when its lock(), try_lock(), and unlock() operations forward to the mutex, and unlocks in its destructor only when it owns the mutex.
- Movable mutex ownership. Explain how moving a std::unique_lock transfers ownership of its mutex, and distinguish automatic moves from moves that require std::move().
- Locks returned across scopes. Describe how a function can acquire a lock, prepare protected data, and return a std::unique_lock so its caller can continue working under the same lock; explain the related gateway-object pattern.
- Early lock release. Explain that std::unique_lock::unlock() can release a mutex before the lock object is destroyed, and why doing so when protection is no longer needed can improve performance.
- Lock scope and contention. Explain that lock granularity concerns both how much data a mutex protects and how long it is held. Describe why work that does not need the shared data, especially slow I/O, should happen outside the lock.
- Snapshot comparison semantics. Explain why comparing values read under separate locks is not necessarily equivalent to comparing them while holding both locks: the reads occur at different times, so a true result need not mean the values were equal at any one instant. State that shortening lock scope must not break the operation's required semantics.
- Initialization-only shared data protection. Explain when shared data needs protection only during initialization: afterward it may be read-only or protected implicitly by its operations. Describe why continuing to lock a mutex would be unnecessary, and state that the C++ Standard provides a mechanism specifically for protecting initialization.
- Double-checked locking data race. Explain why checking the pointer before taking the lock, then checking again under the lock, is not a safe shortcut: the first read is unsynchronized with another thread's write, so a thread might see the pointer without seeing a fully initialized object.
- Once-only synchronized initialization. Describe how std::once_flag and std::call_once provide one-time, properly synchronized initialization, including their use for class members initialized by a member function.
- Reader-writer locking rationale. Explain why a rarely updated data structure needs exclusive access during updates but can allow multiple readers at once, and why an ordinary std::mutex needlessly prevents those concurrent reads.
- DNS cache locking pattern. Describe how the DNS cache uses std::shared_mutex: find_entry() takes a std::shared_lock for read-only access, while update_or_add_entry() takes a std::lock_guard for exclusive access. Explain which operations block when either kind of lock is held.
- Recursive mutex lock ownership. Explain how std::recursive_mutex differs from std::mutex when a thread locks a mutex it already owns, and how repeated locks must be released.
- Nested calls and class invariants. Describe why a thread-safe class might appear to need a recursive mutex when one locking public member function calls another, and justify the recommended private-helper design instead.
What it builds on
- Launching a thread (chapter 2) · Assumes the reader has seen how to start threads.
- Waiting for a thread to complete (chapter 2) · Waiting for a thread to finish with join().
Section by section
Introduction
The chapter introduces the benefits and risks of sharing data between threads and previews ways to protect it.
- Rules for shared data: Explain why easy data sharing is both a benefit and a source of concurrency bugs: threads need rules for when each can access data and how updates are communicated to other threads.
Why it matters: Those rules are the starting point for sharing data safely. - Shared data protection roadmap: Describe the chapter's announced path: examine problems with sharing data, then protect it with mutexes and consider alternative facilities for protection.
Why it matters: This roadmap connects the risks of sharing data to the protection techniques the chapter will discuss.
Key terms: shared data, threads, updates, concurrency-related bugs, mutexes, alternative facilities for protecting shared data
3.1 Problems with sharing data between threads
The section explains how modifying shared data can temporarily break a data structure’s invariants, allowing another thread to observe an inconsistent state.
- Read-only and modified shared data: Distinguish sharing read-only data from sharing data that a thread modifies: concurrent reads do not affect one another, but modifications can create trouble for other threads.
Why it matters: This distinction identifies when sharing data requires special care. - Temporary linked-list invariant break: Trace removal of a node from a doubly linked list and explain why its reciprocal next and previous links cannot both remain consistent throughout the update.
Why it matters: A multi-step update can expose a state that the data structure normally promises never to have. - Consequences of observing partial updates: Explain how a second thread can encounter a partly removed list node and why the result may range from skipping that node to corrupting the list; identify this as an example of a race condition.
Why it matters: The example shows how another thread’s timing can turn a brief inconsistency into a concurrent bug.
Key terms: shared data, read-only, invariants, doubly linked list, next pointer, previous pointer, broken invariant, partially removed, race condition
3.1.1 Race conditions
This section defines race conditions, distinguishes harmless ordering differences from broken invariants, and explains why problematic races can be difficult to reproduce.
- Race conditions and invariants: Explain that a race condition occurs when the outcome depends on the relative order of operations in two or more threads, and distinguish acceptable outcomes from races that break a system invariant.
Why it matters: An ordering-dependent result is a bug only when it violates a requirement of the system. - Partially completed shared updates: Describe how an operation that updates separate pieces of shared data can expose a partially completed state to another thread. Explain why the short timing window makes the failure intermittent and why increased activity or a debugger can change whether it appears.
Why it matters: A rare failure can still be a serious concurrency bug even when ordinary testing does not reproduce it. - Data race distinction: Distinguish the C++ Standard's specific term data race from the broader notion of a race condition: the section identifies it with concurrent modification of a single object and says it causes undefined behavior.
Why it matters: The distinction keeps a general ordering problem separate from the specific C++ error that causes undefined behavior.
Key terms: race condition, invariants, benign race conditions, problematic race conditions, distinct pieces of data, window of opportunity, debugger, data race, undefined behavior
3.1.2 Avoiding problematic race conditions
The section outlines three ways to prevent problematic race conditions and introduces mutexes as the first protection mechanism to examine.
- Protected intermediate states: Explain how protecting a shared data structure prevents other threads from seeing intermediate states that break its invariants, so a modification appears not to have started or to have completed.
Why it matters: This is the basic reason a mutex can make changes to shared data safe for other threads. - Invariant preserving lock-free changes: Describe the alternative of redesigning a data structure so modifications consist of indivisible changes, each preserving its invariants, and explain why this lock-free approach is difficult.
Why it matters: It distinguishes preventing access to intermediate states from designing changes that never expose a broken invariant. - Transactional update and retry: Describe software transactional memory as recording reads and modifications in a transaction log, committing them in one step, and restarting if another thread’s modification prevents the commit. State that the book does not cover STM because C++ has no direct support for it.
Why it matters: The transaction example introduces the alternative of preparing work privately before making it visible at once.
Key terms: protection mechanism, intermediate states, invariants, mutex, lock-free programming, indivisible changes, memory model, software transactional memory, STM, transaction log
3.2 Protecting shared data with mutexes
This section introduces mutexes as a way to protect shared data from race conditions, while warning that correct locking requires careful code and interface design.
- Mutex protection of shared data: Explain how locking a mutex associated with shared data makes access mutually exclusive and prevents other threads from observing the data structure partway through a modification.
Why it matters: This is the basic mechanism behind the locking techniques used throughout the chapter. - Limits of mutex protection: Describe why using a mutex is not, by itself, a complete solution: code must protect the right data, interfaces can retain race conditions, and locking can introduce deadlock or protect too much or too little data.
Why it matters: A lock helps only when the surrounding code and interface use it appropriately.
Key terms: mutex, mutual exclusion, lock, unlock, invariant, race conditions inherent in interfaces, deadlock, protecting either too much or too little data
3.2.1 Using mutexes in C++
The section shows how to use a mutex and RAII locking to protect shared data, and why the interface must not let callers bypass that protection.
- RAII mutex locking: Explain how std::lock_guard<std::mutex> locks a mutex when constructed and unlocks it when destroyed, including when a function exits through an exception.
Why it matters: Automatic unlocking makes it harder to leave shared data permanently locked. - Shared list mutual exclusion: Describe how the list example uses one mutex and lock guards in both add_to_list() and list_contains() so a read cannot occur partway through a modification.
Why it matters: Both readers and writers must follow the same locking rule for the list to be protected. - Protected data interface boundaries: Explain why the mutex and data are usually grouped as private members of a class, and how returning a pointer or reference to the data defeats the protection.
Why it matters: Locking inside methods is insufficient if callers can reach the data without the lock. - C++17 locking syntax: State that C++17 class template argument deduction can omit the explicit template argument for a simple std::lock_guard, and that C++17 also introduces std::scoped_lock.
Why it matters: These names and syntax variations help you recognize equivalent locking code.
Key terms: std::mutex, lock(), unlock(), std::lock_guard, RAII, add_to_list(), list_contains(), mutually exclusive, protected data, private members
3.2.2 Structuring code for protecting shared data
This section shows how pointers or references can let code access shared data outside the mutex meant to protect it.
- Protected data escaping the lock: Explain why locking a member function is insufficient if a pointer or reference to its protected data escapes. Describe how to prevent access outside the lock’s protection.
Why it matters: A mutex protects shared data only when every access to that data is covered by the appropriate lock.
Key terms: std::lock_guard, protected data, pointer, reference, user-supplied functions, scope of the lock
3.2.3 Spotting race conditions inherent in interfaces
The section shows why individually mutex-protected stack operations can still race, then redesigns the stack interface while accounting for exception safety and lock granularity.
- Whole-operation data protection: Explain why a mutex must protect all shared data involved in a complete operation, not merely each individual access. In the linked-list deletion example, that means protecting the node being removed and its neighbors throughout the deletion.
Why it matters: A lock cannot prevent a race if its scope is smaller than the operation that must stay consistent. - Stale stack state checks: Explain why a mutex-protected stack's empty() and size() results cannot guarantee its state when the caller acts on them. Another thread can remove the last item between empty() and top(), making top() on an empty stack undefined behavior.
Why it matters: A correct answer from one method call is not a promise about the next call. - Retrieval and removal conflict: Trace how separate top() and pop() calls let two threads read the same item, process it twice, and discard another item unread. Explain why combining retrieval and removal fixes that race but can lose an item if copying a value for return throws after removal.
Why it matters: A safe stack interface must address both concurrent interleavings and failed copies. - Safe pop interface choices: Describe the proposed ways to combine retrieval and removal without ignoring exception safety: pop() into a caller-supplied reference, restrict value returns to no-throw copyable or movable types, or return a pointer such as std::shared_ptr. State a significant cost or restriction of each and why an interface may offer more than one form.
Why it matters: The best pop() form depends on the stored type and the costs the caller can accept. - Reduced thread-safe stack interface: Describe the example stack's push(), empty(), and two pop() overloads, including its empty_stack exception when pop() finds no item. Explain how removing separate top() and restricting whole-stack operations lets a mutex cover each complete operation, and how the copy constructor locks the source during copying.
Why it matters: The example turns the interface argument into a concrete stack whose operations can be protected end to end. - Lock granularity tradeoffs: Distinguish locks that are too narrow to protect a whole operation from locks so broad that unrelated threads cannot make progress concurrently. Explain why an operation needing multiple mutexes introduces the possibility of deadlock, in which threads wait on one another indefinitely.
Why it matters: Lock scope affects both correctness and whether concurrency provides a performance benefit.
Key terms: mutex, doubly linked list, whole data structure, delete operation, std::stack, empty(), size(), top(), undefined behavior, pop()
3.2.4 Deadlock: the problem and a solution
This section explains how deadlock arises when threads need multiple mutexes and how std::lock and C++17 std::scoped_lock help acquire them safely together.
- Deadlock and lock ordering: Explain how two threads can deadlock when each holds one mutex and waits for the other, and why a consistent lock order prevents that cycle. Describe why ordering mutexes by argument position fails when two threads swap the same objects with reversed arguments.
Why it matters: Recognizing the waiting cycle helps you see why individually reasonable lock acquisitions can stop both threads. - Joint locking and ownership: Describe the swap example's check that the objects differ, its use of std::lock to acquire both mutexes without deadlock, and its use of std::lock_guard with std::adopt_lock to release the already-acquired locks on exit.
Why it matters: Acquiring both locks safely is only part of the job; their ownership must also be managed through normal and exceptional exits. - Joint locking exception behavior: Explain that if std::lock throws while acquiring multiple mutexes, it propagates the exception and releases any mutexes it already acquired.
Why it matters: This all-or-nothing behavior prevents a failed acquisition from leaving an earlier lock held. - Scoped lock alternative and limits: Describe how C++17 std::scoped_lock acquires multiple mutexes with the deadlock-avoiding algorithm and releases them on destruction, usually replacing the separate std::lock and guard steps. State that neither facility prevents deadlock when locks are acquired separately.
Why it matters: The simpler RAII form reduces locking mistakes, but its guarantee applies only to mutexes acquired together.
Key terms: deadlock, mutex, lock order, std::lock, std::lock_guard, std::adopt_lock, std::mutex, all-or-nothing semantics, std::scoped_lock, RAII
3.2.5 Further guidelines for avoiding deadlock
The section explains how wait cycles cause deadlock and gives lock-ordering guidelines, including a runtime-checked mutex hierarchy, for preventing them.
- Deadlock as a wait cycle: Explain that deadlock occurs when threads form a cycle of waiting for one another, even without mutexes, as when each thread tries to join the other. State the guiding rule: do not wait for another thread if it might be waiting for you.
Why it matters: Recognizing wait cycles lets you apply the guidelines to more than mutex locks. - Single-lock and callback precautions: Explain why avoiding nested locks removes deadlock caused solely by mutex acquisition, and why calling user-supplied code while holding a lock can silently reintroduce nesting. Describe std::lock as the option for acquiring multiple locks as one deadlock-avoiding operation.
Why it matters: A function call made under a lock can acquire another lock even when the surrounding code does not do so explicitly. - Consistent lock acquisition order: Explain that when multiple locks cannot be acquired together with std::lock, every thread should acquire them in a consistent order. Use hand-over-hand list traversal or deletion to show how opposing acquisition orders can create a deadlock.
Why it matters: A shared acquisition order prevents threads from each holding a lock the other needs. - Runtime-checked lock hierarchy: Describe a lock hierarchy that assigns mutexes levels and permits a thread holding one hierarchical mutex to acquire only a lower-level one. Explain that this detects ordering violations at runtime, while equal-level simultaneous locking is also disallowed.
Why it matters: A hierarchy turns a lock-ordering convention into a check that does not depend on reproducing a rare deadlock. - Hierarchical mutex state management: Trace how the example hierarchical_mutex checks a thread-local current level before locking, saves the previous level after acquiring its internal mutex, and restores it on unlocking. Distinguish try_lock() failure, which leaves the level unchanged, from successful acquisition.
Why it matters: Tracking and restoring each thread’s level makes the hierarchy enforceable across nested lock scopes.
Key terms: deadlock, wait cycle, join(), nested locks, user-supplied code, std::lock, fixed order, hand-over-hand locking, mutex per node, lock hierarchy
3.2.6 Flexible locking with std::unique_lock
This section explains how std::unique_lock supports deferred locking and tracks ownership, and when that flexibility is worth its cost.
- Deferred locking with unique_lock: Describe how std::defer_lock constructs a std::unique_lock without locking its mutex, and how the lock can later be acquired through the unique_lock or by passing it to std::lock(). Distinguish this from std::adopt_lock, which manages a mutex that is already locked.
Why it matters: Deferred locking lets a lock object be created before the mutex is acquired, as in the swap example. - Tracked mutex ownership: Explain that std::unique_lock tracks whether it currently owns its associated mutex, updates that state when its lock(), try_lock(), and unlock() operations forward to the mutex, and unlocks in its destructor only when it owns the mutex.
Why it matters: Ownership tracking makes deferred locking and later changes in lock ownership safe to manage. - Flexibility and locking cost: Justify choosing std::lock_guard when its simpler behavior suffices, or C++17 std::scoped_lock when available for the multi-lock case, rather than using std::unique_lock without a need for its flexibility. Explain that unique_lock's ownership state makes it typically larger and slightly slower, while deferred locking or transferring ownership can warrant it.
Why it matters: Choosing the simplest suitable lock avoids paying for flexibility the code does not use.
Key terms: std::unique_lock, std::defer_lock, std::adopt_lock, std::lock(), owns_lock(), lock(), try_lock(), unlock(), destructor, std::lock_guard
3.2.7 Transferring mutex ownership between scopes
This section shows how std::unique_lock can transfer mutex ownership across scopes and release a lock before the lock object is destroyed.
- Movable mutex ownership: Explain how moving a std::unique_lock transfers ownership of its mutex, and distinguish automatic moves from moves that require std::move().
Why it matters: Moving a lock lets one scope hand protected work to another without copying lock ownership. - Locks returned across scopes: Describe how a function can acquire a lock, prepare protected data, and return a std::unique_lock so its caller can continue working under the same lock; explain the related gateway-object pattern.
Why it matters: Returning a movable lock preserves protection while responsibility for the protected work changes hands. - Early lock release: Explain that std::unique_lock::unlock() can release a mutex before the lock object is destroyed, and why doing so when protection is no longer needed can improve performance.
Why it matters: Releasing a lock promptly lets waiting threads proceed sooner.
Key terms: std::unique_lock, ownership, std::move(), lvalue, rvalue, moveable but not copyable, get_lock(), process_data(), gateway class, protected data
3.2.8 Locking at an appropriate granularity
This section explains how to reduce lock contention by protecting only the data and operations that need a lock, while preserving the meaning of those operations.
- Lock scope and contention: Explain that lock granularity concerns both how much data a mutex protects and how long it is held. Describe why work that does not need the shared data, especially slow I/O, should happen outside the lock.
Why it matters: Shorter lock holds let other threads reach shared data sooner without leaving required accesses unprotected. - Separate locking for comparisons: Trace the example in which each object's int is copied while its own mutex is held, then the copies are compared after unlocking. Explain why this reduces lock-hold time and avoids holding one mutex while acquiring another.
Why it matters: The example shows a practical way to reduce contention and avoid deadlock when separate snapshots suffice. - Snapshot comparison semantics: Explain why comparing values read under separate locks is not necessarily equivalent to comparing them while holding both locks: the reads occur at different times, so a true result need not mean the values were equal at any one instant. State that shortening lock scope must not break the operation's required semantics.
Why it matters: A change that removes deadlock or contention can still introduce a race in what the operation claims to report.
Key terms: lock granularity, fine-grained lock, coarse-grained lock, contention, std::unique_lock, unlock(), get_detail(), comparison operator, mutex, deadlock
3.3 Alternative facilities for protecting shared data
This section introduces initialization-only protection as an alternative to repeatedly locking a mutex for shared data that needs no explicit synchronization afterward.
- Initialization-only shared data protection: Explain when shared data needs protection only during initialization: afterward it may be read-only or protected implicitly by its operations. Describe why continuing to lock a mutex would be unnecessary, and state that the C++ Standard provides a mechanism specifically for protecting initialization.
Why it matters: Choosing protection that matches when it is needed avoids needless locking overhead.
Key terms: mutexes, shared data, initialization, explicit synchronization
3.3.1 Protecting shared data during initialization
This section explains how to initialize shared resources safely without making every later use wait on a mutex.
- Lazy initialization lock costs: Explain why a mutex around the initialization check is safe but can unnecessarily serialize threads after the resource has been initialized, assuming the resource itself is safe for concurrent access.
Why it matters: The cost of locking on every use motivates a more suitable initialization mechanism. - Double-checked locking data race: Explain why checking the pointer before taking the lock, then checking again under the lock, is not a safe shortcut: the first read is unsynchronized with another thread's write, so a thread might see the pointer without seeing a fully initialized object.
Why it matters: A pointer that appears to have been set does not, by itself, make the initialized object safe to use. - Once-only synchronized initialization: Describe how std::once_flag and std::call_once provide one-time, properly synchronized initialization, including their use for class members initialized by a member function.
Why it matters: This gives threads a safe initialization check without explicitly locking a mutex on every use. - Thread-safe static local initialization: Explain how C++11 initialization of a function-local static provides an alternative for a single global instance: exactly one thread performs initialization, and other threads wait for it to finish.
Why it matters: A local static can provide safe one-time initialization without an explicit once flag in this case.
Key terms: lazy initialization, mutex, serialization, double-checked locking, data race, undefined behavior, std::once_flag, std::call_once, open_connection(), local variable declared with static
3.3.2 Protecting rarely updated data structures
This section shows how a reader-writer mutex protects a rarely updated DNS cache while allowing concurrent reads.
- Reader-writer locking rationale: Explain why a rarely updated data structure needs exclusive access during updates but can allow multiple readers at once, and why an ordinary std::mutex needlessly prevents those concurrent reads.
Why it matters: It explains when shared locking is useful rather than simply locking every access exclusively. - DNS cache locking pattern: Describe how the DNS cache uses std::shared_mutex: find_entry() takes a std::shared_lock for read-only access, while update_or_add_entry() takes a std::lock_guard for exclusive access. Explain which operations block when either kind of lock is held.
Why it matters: The lock chosen for each operation determines whether reads can overlap safely and whether updates remain protected. - Shared mutex performance limits: Explain why using a reader-writer mutex is not automatically faster: its benefit depends on processor count and the relative reader and updater workloads, so performance should be profiled on the target system.
Why it matters: Measurement helps determine whether concurrent reads justify the added locking complexity.
Key terms: reader-writer mutex, exclusive access, shared access, std::mutex, std::shared_mutex, std::shared_lock, std::lock_guard, std::unique_lock, find_entry(), update_or_add_entry()
3.3.3 Recursive locking
This section explains how recursive mutexes permit repeated locking by one thread, but recommends redesigning classes rather than using them to accommodate nested calls between locking member functions.
- Recursive mutex lock ownership: Explain how std::recursive_mutex differs from std::mutex when a thread locks a mutex it already owns, and how repeated locks must be released.
Why it matters: Reacquiring an ordinary mutex is an error, while a recursive mutex remains unavailable to other threads until every acquisition is released. - Nested calls and class invariants: Describe why a thread-safe class might appear to need a recursive mutex when one locking public member function calls another, and justify the recommended private-helper design instead.
Why it matters: A recursive mutex can make a nested call succeed without resolving the risk of calling a public function while the class invariants are broken.
Key terms: std::mutex, std::recursive_mutex, lock(), unlock(), std::lock_guard<std::recursive_mutex>, std::unique_lock<std::recursive_mutex>, public member function, private member function, class invariants
3.4 Summary
This summary recaps how the chapter protects shared data, then explains why threads also need a way to wait efficiently for one another.
Where this is used later
- Locating concurrency-related bugs by testing (chapter 11) · Explains why timing-sensitive concurrency bugs are difficult to reproduce during testing.
- Objects, memory locations, and concurrency (chapter 5) · Develops the standard's treatment of objects, memory locations, and concurrency.
- Writing a thread-safe stack without locks (chapter 7) · Begins the book’s examples of data structures built without locks.
- A thread-safe stack using locks (chapter 6) · Uses locks to protect a concurrent stack.
- (chapter intro) (chapter 4) · Introduces protecting shared data with mutexes.
- (chapter intro) (chapter 6) · Introduces mutexes for protecting shared data.
- Guidelines for designing data structures for concurrency (chapter 6) · Designing concurrent data structures requires checking that their interfaces do not expose protected data.
- The std::atomic<> primary class template (chapter 5) · The guideline against passing references to protected data into user-supplied functions.
Concepts explained in this chapter
- deadlockDeadlock is a state in which two or more threads are indefinitely blocked, each waiting for a resource or event that another blocked thread must provide.
- race conditionA race condition is a program condition in which behavior depends on the timing or ordering of operations performed by concurrent threads.
- locking granularityLock granularity is the amount of data protected by a single lock.
- lock hierarchyA convention in which threads acquire multiple locks in a consistent order to prevent circular waiting.
- thread-safe stackThe thread-safe stack is the example application's stack type that coordinates access among threads and throws when empty.
- lock_guardA C++ standard library class template that acquires a lock on construction and releases it on destruction.
- mutexA synchronization primitive used to coordinate exclusive access to a shared resource.
- unique_lock`std::unique_lock` is a C++ standard library class template that manages ownership of a mutex lock and supports deferred locking, unlocking, and ownership transfer.
- std::scoped_lockA C++ standard library class template that acquires ownership of one or more lockable objects and releases them on destruction.
- 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.
- invariantAn invariant is a condition that remains true about a data structure or program state when it is in a valid state.
- std::lock`std::lock` is a C++ standard library function template that locks multiple lockable objects together while avoiding deadlock among those objects.
- call_once`std::call_once` is a C++ standard library function template that ensures a callable is executed exactly once for a given `std::once_flag`.
- shared_mutexA C++ standard library mutex type that supports either exclusive ownership by one thread or shared ownership by multiple threads.
- double-checked locking patternDouble-checked locking is a synchronization pattern that checks a shared pointer before and after acquiring a lock to avoid locking when initialization is complete.
- lazy initializationLazy initialization is a technique that delays constructing or initializing a resource until it is first required.
- std::once_flag`std::once_flag` is a noncopyable C++ standard library type whose instances coordinate one-time execution with `std::call_once`.
- mutable shared stateShared mutable data is information accessible by multiple threads that can also be modified during execution.
- ownership transferOwnership transfer is the reassignment of responsibility for managing a resource, such as a lock, from one entity to another.
- fine-grained lockingFine-grained locking protects smaller portions of shared data with separate locks rather than one broad lock.
- resource acquisition is initializationResource Acquisition Is Initialization is a C++ programming idiom that binds resource ownership and release to object lifetime.
- lockless techniqueA progress guarantee under which, when threads continue taking steps, at least one operation completes despite contention.
- concurrent accessConcurrent access is access to the same data by multiple threads during overlapping periods of execution.
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.
