Chapter 4: Chapter 4. Synchronizing concurrent operations
The chapter introduces waiting for other threads’ actions and surveys the C++ facilities used to synchronize them.
After this chapter you will be able to
- Protection versus operation synchronization. Distinguish protecting shared data from coordinating actions between threads, such as waiting for another thread to finish a task or for a condition to become true. Explain why repeatedly checking a shared completion flag is not the preferred approach.
- Condition variable event waiting. Explain that a condition variable lets one or more threads wait for a condition, and that a thread finding the condition satisfied can notify waiting threads to wake and continue.
- Queue notification and waiting. Trace the queue example: the producer locks the queue mutex, adds data, unlocks, and calls notify_one(); the consumer holds a std::unique_lock and waits until the queue is nonempty. Explain that wait releases the mutex while blocked and reacquires it before returning.
- Predicate checks and spurious wakes. Explain that wait checks its supplied predicate with the mutex locked, returns only when the predicate is true, and may check it repeatedly, including after a spurious wake. State why a predicate without side effects is advisable.
- Race-safe queue removal interface. Explain why a thread-safe queue combines reading and removing an item, and distinguish try_pop() from wait_and_pop() and their ways of returning a result.
- Encapsulated queue synchronization. Describe how the queue keeps its mutex and condition variable inside the object, so push() needs no external synchronization and wait_and_pop() handles waiting. Explain why empty() and the copy constructor still lock, requiring a mutable mutex.
- One-off event futures. Explain how a future represents a one-off event: a thread can check it periodically or wait when it needs the event to have occurred, and the ready future cannot be reset. Describe how the event may carry a value, such as the result of a background calculation, or no value.
- Unique and shared futures. Distinguish std::future<> from std::shared_future<> by whether one instance or multiple instances can refer to the same event. Explain that future objects do not themselves synchronize concurrent access to one object, whereas threads may access separate copies of a shared future referring to the same result.
- Asynchronous results with futures. Explain why std::async is useful when a calculation’s result is not needed immediately, and how its returned std::future provides that result through get().
- Deferred and asynchronous launch policies. Distinguish the std::launch choices for std::async: deferred execution on a wait or get, execution on its own thread, and the default implementation choice between them. Explain why a deferred task might never run.
- Packaged task future lifecycle. Explain how std::packaged_task wraps a callable, supplies its future through get_future(), and makes that future ready with the callable’s result when the task is invoked.
- GUI thread task handoff. Trace the example in which a caller queues a std::packaged_task<void()> for the GUI thread and receives a future that can indicate completion.
- Promise and future pairing. Explain how std::promise<T> supplies a value to an associated std::future<T>: get_future() obtains the future, and set_value() stores the result and makes the future ready.
- Connection processing with promises. Trace how process_connections() handles multiple connections: it checks for incoming and queued outgoing packets, sets a promise mapped from an incoming packet's ID to its payload, and sets an outgoing packet's promise to true after a successful send.
- Exceptions from asynchronous tasks. Explain how exceptions thrown by functions run through std::async or std::packaged_task reach the thread that retrieves the result.
- Explicit promise exception storage. Describe how to put an exception into a future through std::promise, using either a caught exception or a newly created exception.
- Unique and shared result ownership. Distinguish std::future’s unique ownership and one-shot get() from std::shared_future’s copyable references to the same asynchronous state.
- Safe access through separate copies. Explain that sharing an asynchronous state is safe through separate std::shared_future objects, but concurrent access to the same future object’s member functions still needs a lock. Describe passing each thread its own copy, as in tasks waiting for spreadsheet-cell dependencies.
- Duration and absolute timeouts. Distinguish waiting for an amount of time from waiting until a specified point in time, and identify the corresponding _for and _until suffixes.
- Clock interface and tick period. Describe the four pieces of information a clock class provides: the current time, its time-point type, its tick period, and whether it is steady. Explain that the specified period is not a guarantee of the tick period observed at runtime.
- Steady clocks and timeouts. Explain that a steady clock ticks at a uniform rate and cannot be adjusted, making it important for timeout calculations. Distinguish it from an adjustable system clock whose now() value can move backward.
- Duration representation and units. Explain that std::chrono::duration stores a count in a chosen representation type and uses a ratio to specify how many seconds each unit represents.
- Duration conversion and arithmetic. Explain when duration conversion is implicit, how duration_cast handles a conversion that may lose precision, and how to perform arithmetic or retrieve a duration's count.
- Duration based wait behavior. Describe how a duration limits a wait for a future, what its returned statuses mean, and why elapsed time may differ from the requested duration.
- Time point arithmetic. Explain that adding or subtracting a duration moves a time point, while subtracting two time points on the same clock yields the duration between them. Describe using now() plus a duration to set an absolute deadline.
- Bounded condition variable waits. Justify calculating one deadline before a condition-variable wait loop and passing it to an _until wait on each iteration when no predicate is passed. Explain that looping handles spurious wakeups, whereas restarting a full wait_for() duration after each wakeup can leave the total wait unbounded.
- Thread sleep timing choices. Explain how sleeping a thread avoids consuming processing time while it has nothing to do, and distinguish sleeping for a duration from sleeping until a particular time point.
- Timeout capable synchronization functions. Describe which other synchronization facilities accept timeouts, including condition variables, futures, and timed mutexes. Distinguish timed mutexes from plain mutexes and explain the duration and time-point forms of timed lock attempts.
- Task-focused synchronization with futures. Explain how synchronization facilities let programmers focus on which operations must be coordinated rather than on synchronization mechanics, and describe the functional approach of giving each task its input and passing results to other threads through futures.
- Pure functions and shared state. Explain that a pure function’s result depends only on its arguments and that it does not modify external state. Describe why avoiding shared-data modifications simplifies concurrent code.
- Futures in parallel Quicksort. Trace the change from sequential recursion to launching the lower-part sort with std::async, sorting the higher part directly, and calling get() on the lower-part future before assembling the result.
- Message passing and shared state. Explain how a thread in the CSP style responds to messages by updating its own state and possibly sending messages, rather than directly sharing application data. State that C++ cannot enforce the absence of shared data, so programmers must maintain that discipline while a library handles synchronization inside shared message queues.
- ATM actors and state transitions. Describe the ATM split into separate machinery, logic, and bank-communication threads that exchange messages. Trace how the logic moves from waiting for a card through PIN entry and verification to cancellation or withdrawal, using bank confirmation before dispensing cash.
- ATM message handler loop. Trace how run() repeatedly calls the current state function, which waits for specified message types and uses handlers to update data, send messages, or change state. Explain that chained wait().handle() calls accept the listed types and discard others, and that a handled message need not change state.
- Continuation-based result processing. Explain how std::experimental::future::then() attaches processing to a future so it runs when the result is ready, rather than requiring the caller to wait. State that the continuation may run on an unspecified thread.
- Future consumption and chaining. Describe why attaching a continuation invalidates the original future and how then() returns a new future for the continuation's result, allowing further continuations to be chained.
- Continuation input and exceptions. Explain that a continuation receives the ready future as its sole argument, not the result value, so it can handle either a value or an exception. Describe how get() can rethrow a stored exception and how an exception escaping the continuation is stored in its result future.
- Blocked thread motivation. Explain why moving an entire sequential login workflow into one std::async task frees the UI thread but can waste background threads, and how continuations address that problem.
- Continuation inputs and exceptions. Trace how each continuation receives a std::experimental::future, calls get() for its predecessor's result, and passes exceptions down to the final handler.
- Asynchronous calls and future unwrapping. Explain why backend calls must return futures rather than block internally, and how future-unwrapping keeps a continuation that returns a future from producing a nested future.
- Cost of individual future waits. Explain why gathering independently processed results by waiting on each future in a separate asynchronous task can be inefficient.
- All-results future and continuation. Describe how std::experimental::when_all produces a future that is ready once every supplied future is ready, allowing a .then continuation to gather and process their results without blocking on each get call.
- First ready future result. Explain that std::experimental::when_any returns a new future that becomes ready when at least one input future is ready. Describe how its when_any_result contains the collection of futures and an index identifying which one triggered readiness.
- Repeated search result checks. Trace the parallel search example: tasks search separate chunks, and a continuation checks the future that became ready. If that task found no value, the continuation removes its future and calls when_any again; a found value sets the final result, while exhausting the futures stores an exception.
- Latch counter and readiness. Explain that a latch becomes ready when its counter reaches zero and stays ready until destroyed. Describe how decrements can come from one thread, multiple threads, or a combination.
- Barrier arrival cycles. Describe how each thread arrives at a barrier once per cycle and blocks until all participating threads have arrived. Explain that the threads are then released and can use the barrier again for the next cycle.
- Latch counter and operations. Describe how to construct a std::experimental::latch with an initial event count, call count_down as events occur, and use wait or is_ready to wait for or check readiness. Explain that count_down_and_wait combines signaling an event with waiting.
- Data readiness versus task completion. Trace the example in which async tasks count down the latch after generating their data but continue processing, while the main thread waits on the latch before processing that data. Distinguish this readiness point from completion of every task, which the future destructors ensure at the end of the function.
- Latch synchronization and visibility. Explain why the main thread can safely process data written by other threads after waiting on the same latch: count_down synchronizes with wait, making the workers' changes visible when wait returns.
- Barrier cycles and membership. Explain how std::experimental::barrier releases a group of threads when all have arrived, then resets for another cycle. Distinguish it from a latch and describe what happens when a thread calls arrive_and_drop.
- Data processing synchronization points. Trace the example’s serial, parallel, and output stages, explaining why every thread calls arrive_and_wait before parallel processing and again before thread 0 writes the result.
- Barrier completion function. Explain that flex_barrier adds a constructor taking a thread count and a completion function, which runs on exactly one arriving thread after all participating threads have arrived.
- Next-cycle thread count. Describe how the completion function's return value sets the required arrivals for the next barrier cycle: -1 keeps the count unchanged, while zero or more specifies a new count. Explain that the programmer must ensure the correct number of threads arrives next time.
What it builds on
- Protecting shared data with mutexes (chapter 3) · Introduces protecting shared data with mutexes.
- Using mutexes in C++ (chapter 3) · Using a mutex to protect shared data.
- Flexible locking with std::unique_lock (chapter 3) · Using std::unique_lock when a lock must be unlocked and locked again.
- Locking at an appropriate granularity (chapter 3) · Holding a mutex only at an appropriate granularity.
- Spotting race conditions inherent in interfaces (chapter 3) · Introduces race conditions inherent in separate read-and-remove operations.
- Launching a thread (chapter 2) · Introduces launching work in a std::thread.
- Waiting for a thread to complete (chapter 2) · Introduces waiting for a std::thread to complete.
- Passing arguments to a thread function (chapter 2) · Introduces passing arguments to thread functions.
Section by section
Introduction
The chapter introduces waiting for other threads’ actions and surveys the C++ facilities used to synchronize them.
- Protection versus operation synchronization: Distinguish protecting shared data from coordinating actions between threads, such as waiting for another thread to finish a task or for a condition to become true. Explain why repeatedly checking a shared completion flag is not the preferred approach.
Why it matters: Protecting data alone does not let a thread efficiently wait for another thread’s progress. - Synchronization facilities and chapter scope: Describe the chapter’s survey of condition variables and futures for waiting, including time-limited waits and ways to simplify synchronization. State that the Concurrency Technical Specification adds future operations, latches, and barriers.
Why it matters: This overview connects the need to wait with the facilities examined in the chapter.
Key terms: shared data, synchronize actions, event, condition, condition variables, futures, Concurrency Technical Specification (TS), latches, barriers
4.1 Waiting for an event or other condition
This section compares ways for one thread to wait for another and introduces condition variables as the preferred way to wait for an event.
- Repeated flag checking costs: Explain why repeatedly checking a mutex-protected completion flag is wasteful: the waiting thread consumes processing time and may prevent the other thread from locking the mutex to set the flag.
Why it matters: A wait can make the task it depends on slower to finish. - Timed polling tradeoff: Describe how sleeping between flag checks reduces processing waste, why the mutex is unlocked during the sleep, and why choosing the sleep period remains difficult.
Why it matters: Sleeping helps, but a poorly chosen interval can still waste work or delay a response. - Condition variable event waiting: Explain that a condition variable lets one or more threads wait for a condition, and that a thread finding the condition satisfied can notify waiting threads to wake and continue.
Why it matters: It provides the event-driven alternative to repeatedly checking or guessing when to check again.
Key terms: flag, mutex, waiting thread, std::this_thread::sleep_for(), sleep period, condition variable, condition, notify
4.1.1 Waiting for a condition with condition variables
This section shows how a producer and consumer use a condition variable, a mutex, and a predicate to wait for queued data without holding the mutex while idle.
- Condition variable choices: Distinguish std::condition_variable from std::condition_variable_any: both require a mutex, but the former works with std::mutex while the latter accepts other mutex-like types. Explain why the simpler type is preferred unless that flexibility is needed.
Why it matters: Choosing the simpler type avoids potential size, performance, or resource costs when extra flexibility is unnecessary. - Queue notification and waiting: Trace the queue example: the producer locks the queue mutex, adds data, unlocks, and calls notify_one(); the consumer holds a std::unique_lock and waits until the queue is nonempty. Explain that wait releases the mutex while blocked and reacquires it before returning.
Why it matters: The handoff works because the producer can acquire the mutex while the consumer is waiting. - Predicate checks and spurious wakes: Explain that wait checks its supplied predicate with the mutex locked, returns only when the predicate is true, and may check it repeatedly, including after a spurious wake. State why a predicate without side effects is advisable.
Why it matters: The condition, rather than a notification alone, determines whether the waiting thread can safely proceed. - Short queue locking scope: Explain why the producer unlocks before notifying and why the consumer unlocks after taking the data but before potentially lengthy processing. Relate both choices to avoiding unnecessary time holding or waiting for the queue mutex.
Why it matters: Keeping the lock confined to queue operations lets the other thread make progress sooner.
Key terms: std::condition_variable, std::condition_variable_any, std::mutex, std::lock_guard, std::unique_lock, wait(), notify_one(), data_queue, predicate, spurious wake
4.1.2 Building a thread-safe queue with condition variables
This section designs a thread-safe queue around condition variables, then explains how to notify one or all waiting threads and when a future may be a better fit.
- Race-safe queue removal interface: Explain why a thread-safe queue combines reading and removing an item, and distinguish try_pop() from wait_and_pop() and their ways of returning a result.
Why it matters: A safe interface prevents another thread from changing the queue between a read and a removal. - Encapsulated queue synchronization: Describe how the queue keeps its mutex and condition variable inside the object, so push() needs no external synchronization and wait_and_pop() handles waiting. Explain why empty() and the copy constructor still lock, requiring a mutable mutex.
Why it matters: Keeping synchronization inside the queue lets callers use its operations without coordinating access themselves. - Single versus broadcast notification: Distinguish notify_one() for work that only one waiting processing thread should handle from notify_all() for an event that every waiting thread must respond to.
Why it matters: The choice of notification determines whether one worker or every waiting thread checks the changed condition. - One-off event synchronization choice: Explain why a future may be more appropriate than a condition variable when a thread waits only once for a particular piece of data.
Why it matters: Recognizing a one-time data handoff helps select the synchronization facility suited to it.
Key terms: front(), pop(), try_pop(), wait_and_pop(), threadsafe_queue, mutex, condition variable, push(), mutable, notify_one()
4.2 Waiting for one-off events with futures
Introduces futures as a way to wait for one-off events and receive their results, then distinguishes unique, shared, and experimental futures.
- One-off event futures: Explain how a future represents a one-off event: a thread can check it periodically or wait when it needs the event to have occurred, and the ready future cannot be reset. Describe how the event may carry a value, such as the result of a background calculation, or no value.
Why it matters: This explains how a thread can wait for a background result that std::thread does not easily return. - Unique and shared futures: Distinguish std::future<> from std::shared_future<> by whether one instance or multiple instances can refer to the same event. Explain that future objects do not themselves synchronize concurrent access to one object, whereas threads may access separate copies of a shared future referring to the same result.
Why it matters: The distinction determines how several threads can safely wait for and access the same asynchronous result. - Experimental future variants: Describe the Concurrency TS future variants in std::experimental as counterparts with additional member functions, available through <experimental/future>. Explain that experimental marks them as non-standard with potentially changing syntax or semantics, not as low-quality implementations.
Why it matters: This helps distinguish the standard future API from the extended API used for later facilities.
Key terms: future, one-off event, ready, std::future<>, std::shared_future<>, <future>, std::future<void>, std::shared_future<void>, Concurrency TS, std::experimental::future<>
4.2.1 Returning values from background tasks
This section shows how std::async returns a future for a task’s result, how to pass arguments to the task, and how launch policies affect when it runs.
- Asynchronous results with futures: Explain why std::async is useful when a calculation’s result is not needed immediately, and how its returned std::future provides that result through get().
Why it matters: A future lets you retrieve a task’s return value without arranging a separate transfer from a std::thread. - Task arguments and member functions: Describe how std::async passes extra arguments to a function or callable, including the object argument required for a member-function pointer and the use of moves for rvalue arguments.
Why it matters: These calling rules let asynchronous tasks use ordinary functions, member functions, and move-only values. - Deferred and asynchronous launch policies: Distinguish the std::launch choices for std::async: deferred execution on a wait or get, execution on its own thread, and the default implementation choice between them. Explain why a deferred task might never run.
Why it matters: Calling std::async alone does not guarantee that work begins on another thread. - Other task future connections: State that std::async is not the only way to associate a future with a task: std::packaged_task wraps a task, while std::promise supports explicitly setting a value.
Why it matters: These alternatives lead into the chapter’s other ways of delivering one-off results.
Key terms: std::async, std::future, get(), std::thread, pointer to a member function, std::ref, rvalues, move-only types, std::launch::deferred, std::launch::async
4.2.2 Associating a task with a future
This section shows how std::packaged_task connects a callable to a future and lets one thread submit work for another thread to run.
- Packaged task future lifecycle: Explain how std::packaged_task wraps a callable, supplies its future through get_future(), and makes that future ready with the callable’s result when the task is invoked.
Why it matters: The caller can keep the future while handing the task to another thread or a scheduler. - Packaged task function signature: Describe how the std::packaged_task<> template’s function signature determines both the future’s result type and the arguments used to invoke the task, and how a compatible callable can have implicitly convertible types.
Why it matters: The signature determines how callers invoke the task and what result they can obtain. - GUI thread task handoff: Trace the example in which a caller queues a std::packaged_task<void()> for the GUI thread and receives a future that can indicate completion.
Why it matters: It shows how to run work on a required thread without defining a separate message type for every GUI action.
Key terms: std::packaged_task<>, get_future(), std::future<>, callable object, asynchronous result, function signature, return type, argument list, GUI thread, task queue
4.2.3 Making (std::)promises
This section explains how promises and futures let a small number of connection-handling threads deliver results to other threads waiting for network operations.
- Promise and future pairing: Explain how std::promise<T> supplies a value to an associated std::future<T>: get_future() obtains the future, and set_value() stores the result and makes the future ready.
Why it matters: The pair lets one thread wait for a result that another thread will provide. - Connection processing with promises: Trace how process_connections() handles multiple connections: it checks for incoming and queued outgoing packets, sets a promise mapped from an incoming packet's ID to its payload, and sets an outgoing packet's promise to true after a successful send.
Why it matters: The example shows how independent network operations can deliver their results to the right waiting code. - Thread allocation for connections: Justify handling many network connections with a small number of threads rather than one thread per connection, citing thread resource costs and context switching when thread counts exceed available hardware concurrency.
Why it matters: This explains why the example needs a way to notify waiters without dedicating a thread to every connection. - Unset promises and errors: State that destroying a std::promise without setting a value stores an exception instead, and explain why asynchronous results need a way to convey failures as well as successful values.
Why it matters: Waiting code still needs to learn when an operation fails rather than produces a value.
Key terms: std::promise<T>, std::future<T>, get_future(), set_value(), process_connections(), incoming packet, ID, payload, outgoing queue, std::promise<bool>
4.2.4 Saving an exception for the future
This section explains how futures deliver exceptions from asynchronous work, including explicitly stored exceptions and broken promises.
- Exceptions from asynchronous tasks: Explain how exceptions thrown by functions run through std::async or std::packaged_task reach the thread that retrieves the result.
Why it matters: An asynchronous failure can be handled where the result is consumed, much like a direct function-call failure. - Explicit promise exception storage: Describe how to put an exception into a future through std::promise, using either a caught exception or a newly created exception.
Why it matters: A promise can report failure to its future's consumer rather than supplying a value. - Broken promise failure: Explain what happens when a promise or packaged task is destroyed without supplying a result, and why the future must be made ready in that case.
Why it matters: A waiting thread needs a definite failure instead of waiting indefinitely for a result that will never arrive. - Single waiter future limitation: Distinguish std::future from std::shared_future for waiting on the same event from multiple threads.
Why it matters: The number of threads that need the result determines which kind of future to use.
Key terms: std::async, std::packaged_task, std::future, get(), std::promise, set_exception(), set_value(), std::current_exception(), std::make_exception_ptr(), std::future_error
4.2.5 Waiting from multiple threads
The section explains how multiple threads can wait for one asynchronous result safely using std::shared_future, and how to create one from a std::future.
- Unique and shared result ownership: Distinguish std::future’s unique ownership and one-shot get() from std::shared_future’s copyable references to the same asynchronous state.
Why it matters: This distinction determines whether one result can be used by multiple threads. - Safe access through separate copies: Explain that sharing an asynchronous state is safe through separate std::shared_future objects, but concurrent access to the same future object’s member functions still needs a lock. Describe passing each thread its own copy, as in tasks waiting for spreadsheet-cell dependencies.
Why it matters: A shared result does not make concurrent access to one future object safe. - Transfer into a shared future: Describe creating a std::shared_future by transferring a std::future’s asynchronous state with std::move, from an rvalue future, or with share(), and state that the original future becomes invalid after transfer.
Why it matters: The conversion transfers ownership rather than leaving both kinds of future owning the state.
Key terms: std::future, std::shared_future, asynchronous state, get(), data race, lock, std::move, get_future(), share(), valid
4.3 Waiting with a time limit
This section introduces why a thread might limit a blocking wait, the two forms of timeout, and how condition-variable waits use them.
- Reasons for bounded waits: Explain why an indefinite wait may be unsuitable: a timeout lets a program report that it is still alive or stop waiting after a user cancels.
Why it matters: A time limit lets a program remain responsive even when the event it awaits has not occurred. - Duration and absolute timeouts: Distinguish waiting for an amount of time from waiting until a specified point in time, and identify the corresponding _for and _until suffixes.
Why it matters: The two timeout forms express different limits and lead to different waiting-function variants. - Condition variable timeout overloads: Describe how std::condition_variable offers wait_for() and wait_until() overloads corresponding to its two wait() overloads: one can wake on a signal, timeout, or spurious wakeup; the predicate version checks the predicate on waking and returns when it is true or the timeout expires.
Why it matters: The predicate overload changes when a timed wait returns, so a wakeup alone is not necessarily completion.
Key terms: blocking calls, timeout, duration-based timeout, absolute timeout, _for, _until, std::condition_variable, wait_for(), wait_until(), predicate
4.3.1 Clocks
This section explains what information a C++ clock provides and why a steady clock matters when measuring timeouts.
- Clock interface and tick period: Describe the four pieces of information a clock class provides: the current time, its time-point type, its tick period, and whether it is steady. Explain that the specified period is not a guarantee of the tick period observed at runtime.
Why it matters: These clock properties give meaning to the time points and durations used in timed operations. - Steady clocks and timeouts: Explain that a steady clock ticks at a uniform rate and cannot be adjusted, making it important for timeout calculations. Distinguish it from an adjustable system clock whose now() value can move backward.
Why it matters: A clock adjustment can make elapsed-time calculations unreliable. - Standard clock alternatives: Distinguish the library's system_clock, steady_clock, and high_resolution_clock by their stated purposes, including that high_resolution_clock has the smallest possible tick period among supplied clocks and may be a typedef for another clock.
Why it matters: The clock with the finest resolution is not identified by that property alone as the steady choice.
Key terms: now(), time_point, period, is_steady, std::ratio, steady clock, std::chrono::steady_clock, std::chrono::system_clock, std::chrono::high_resolution_clock, time_t
4.3.2 Durations
This section explains how C++ represents and manipulates durations and uses them to limit how long a thread waits.
- Duration representation and units: Explain that std::chrono::duration stores a count in a chosen representation type and uses a ratio to specify how many seconds each unit represents.
Why it matters: The representation and ratio determine what a duration's count means. - Predefined durations and literals: Describe the predefined chrono duration types and C++14 literal suffixes, distinguishing integer literals from floating-point literals and noting when an explicitly chosen floating-point representation is preferable.
Why it matters: Convenience types and literals make time values readable, but a floating-point literal leaves its representation type to the implementation. - Duration conversion and arithmetic: Explain when duration conversion is implicit, how duration_cast handles a conversion that may lose precision, and how to perform arithmetic or retrieve a duration's count.
Why it matters: These rules help prevent accidental loss of time units while allowing durations to be calculated and inspected. - Duration based wait behavior: Describe how a duration limits a wait for a future, what its returned statuses mean, and why elapsed time may differ from the requested duration.
Why it matters: A timeout is an elapsed-time limit for waiting, not a guarantee about exactly when the waiting thread resumes.
Key terms: std::chrono::duration<>, representation, std::ratio, std::chrono::nanoseconds, std::chrono::milliseconds, std::chrono_literals, literal suffix operators, std::chrono::duration_cast<>, count(), truncated
4.3.3 Time points
This section explains how chrono time points represent absolute times and why a fixed deadline is useful for bounded waits.
- Time point representation: Describe a std::chrono::time_point as a value tied to a clock and a duration unit, measured from that clock’s epoch. Explain that time_since_epoch() returns the elapsed duration even though the epoch itself cannot be queried directly.
Why it matters: The clock and units determine what a time point means and how it can be used. - Time point arithmetic: Explain that adding or subtracting a duration moves a time point, while subtracting two time points on the same clock yields the duration between them. Describe using now() plus a duration to set an absolute deadline.
Why it matters: A calculated deadline lets several operations share one time budget. - Clock behavior during deadlines: Explain that an absolute-timeout wait uses the clock associated with its time point to decide when the deadline has passed. Describe how adjusting that clock forward can shorten the wait and adjusting it backward can lengthen the wait when measured by a steady clock.
Why it matters: A deadline’s clock affects how long a wait lasts if that clock changes. - Bounded condition variable waits: Justify calculating one deadline before a condition-variable wait loop and passing it to an _until wait on each iteration when no predicate is passed. Explain that looping handles spurious wakeups, whereas restarting a full wait_for() duration after each wakeup can leave the total wait unbounded.
Why it matters: Keeping the same deadline prevents repeated wakeups from repeatedly resetting the time limit.
Key terms: std::chrono::time_point<>, clock, duration, epoch, time_since_epoch(), now(), absolute timeout, clock parameter, steady clock, _until
4.3.4 Functions that accept timeouts
This section shows where C++ concurrency functions accept timeouts and how to choose between waiting for a duration and waiting until a time point.
- Thread sleep timing choices: Explain how sleeping a thread avoids consuming processing time while it has nothing to do, and distinguish sleeping for a duration from sleeping until a particular time point.
Why it matters: Choosing the right sleep function lets a thread pause without repeatedly checking for work. - Timeout capable synchronization functions: Describe which other synchronization facilities accept timeouts, including condition variables, futures, and timed mutexes. Distinguish timed mutexes from plain mutexes and explain the duration and time-point forms of timed lock attempts.
Why it matters: Not every mutex supports a timed lock attempt, even though other waiting facilities accept timeouts.
Key terms: std::this_thread::sleep_for(), std::this_thread::sleep_until(), duration, time point, condition variables, futures, std::timed_mutex, std::recursive_timed_mutex, std::mutex, std::recursive_mutex
4.4 Using synchronization of operations to simplify code
This section introduces how synchronization facilities can simplify concurrent code by letting tasks receive the data they need and communicate results through futures instead of directly sharing data.
- Task-focused synchronization with futures: Explain how synchronization facilities let programmers focus on which operations must be coordinated rather than on synchronization mechanics, and describe the functional approach of giving each task its input and passing results to other threads through futures.
Why it matters: This approach can make concurrent code easier to reason about by reducing direct data sharing between threads.
Key terms: synchronization facilities, functional programming, futures
4.4.1 Functional programming with futures
The section uses functional-style Quicksort to show how futures can coordinate parallel work without explicitly sharing mutable data, while noting the limits of task spawning.
- Pure functions and shared state: Explain that a pure function’s result depends only on its arguments and that it does not modify external state. Describe why avoiding shared-data modifications simplifies concurrent code.
Why it matters: This explains why the example can focus on combining results rather than protecting shared data. - Functional-style sequential Quicksort: Trace how the sequential example takes and returns a list by value, partitions around a pivot, recursively sorts the lower and higher parts, and assembles the result. Distinguish its functional-style interface from its imperative, move-and-splice-based internals.
Why it matters: The sequential version establishes the steps that the futures-based version changes. - Futures in parallel Quicksort: Trace the change from sequential recursion to launching the lower-part sort with std::async, sorting the higher part directly, and calling get() on the lower-part future before assembling the result.
Why it matters: The example shows how one computation can depend on another’s result without explicitly accessing shared data. - Task spawning and scaling limits: Explain why recursive std::async calls may create too many threads and why the library cannot be assumed to choose an optimal execution strategy. Describe how a spawn_task wrapper could use std::packaged_task, std::thread, and a future, while a task queue and worker pool would offer a path to more control.
Why it matters: A working futures-based algorithm is not automatically an efficiently scaled parallel algorithm.
Key terms: functional programming, pure function, shared data, race conditions, mutexes, Quicksort, pivot, std::partition, splice(), std::move()
4.4.2 Synchronizing operations with message passing
The section shows how message passing lets C++ threads act as independent state machines, using an ATM to illustrate the Actor model and its message handlers.
- Message passing and shared state: Explain how a thread in the CSP style responds to messages by updating its own state and possibly sending messages, rather than directly sharing application data. State that C++ cannot enforce the absence of shared data, so programmers must maintain that discipline while a library handles synchronization inside shared message queues.
Why it matters: Keeping application state separate lets you reason about each thread largely on its own. - ATM actors and state transitions: Describe the ATM split into separate machinery, logic, and bank-communication threads that exchange messages. Trace how the logic moves from waiting for a card through PIN entry and verification to cancellation or withdrawal, using bank confirmation before dispensing cash.
Why it matters: The ATM makes the division of responsibilities and message-driven state changes concrete. - ATM message handler loop: Trace how run() repeatedly calls the current state function, which waits for specified message types and uses handlers to update data, send messages, or change state. Explain that chained wait().handle() calls accept the listed types and discard others, and that a handled message need not change state.
Why it matters: This is how the state-machine design becomes executable message-passing code.
Key terms: CSP, message queues, shared data, state machine, Actor model, ATM logic, waiting_for_card, getting_pin, run(), wait()
4.4.3 Continuation-style concurrency with the Concurrency TS
This section introduces Concurrency TS futures and continuations, showing how to process a result when it becomes ready without explicitly waiting for it.
- Continuation-based result processing: Explain how std::experimental::future::then() attaches processing to a future so it runs when the result is ready, rather than requiring the caller to wait. State that the continuation may run on an unspecified thread.
Why it matters: Continuations let you express what should happen next without tying up the calling code in a wait. - Future consumption and chaining: Describe why attaching a continuation invalidates the original future and how then() returns a new future for the continuation's result, allowing further continuations to be chained.
Why it matters: Following which future remains valid prevents attempts to retrieve a result that the continuation has consumed. - Continuation input and exceptions: Explain that a continuation receives the ready future as its sole argument, not the result value, so it can handle either a value or an exception. Describe how get() can rethrow a stored exception and how an exception escaping the continuation is stored in its result future.
Why it matters: Passing the future lets follow-up code decide what to do when the preceding task fails. - Concurrency TS async substitute: Describe the section's substitute for std::async: obtain a future from an std::experimental::promise, run the supplied function in a new thread, and store either its return value or its exception in the promise. Explain why the example uses the at-thread-exit setters.
Why it matters: The example shows how to produce a Concurrency TS future even though the TS does not specify an async equivalent.
Key terms: std::experimental::future, continuation, then(), unspecified thread, invalid future, continuation result, chain continuations, ready future, std::experimental::future<int>, get()
4.4.4 Chaining continuations
This section shows how to chain future continuations so a login workflow can proceed asynchronously, propagate exceptions, and branch from a shared future.
- Blocked thread motivation: Explain why moving an entire sequential login workflow into one std::async task frees the UI thread but can waste background threads, and how continuations address that problem.
Why it matters: Freeing the UI thread is not enough if many background threads remain idle while waiting. - Continuation inputs and exceptions: Trace how each continuation receives a std::experimental::future, calls get() for its predecessor's result, and passes exceptions down to the final handler.
Why it matters: A chained workflow can handle failures from any step at its final display stage. - Asynchronous calls and future unwrapping: Explain why backend calls must return futures rather than block internally, and how future-unwrapping keeps a continuation that returns a future from producing a nested future.
Why it matters: Future-unwrapping lets the chain remain straightforward without tying up threads on backend waits. - Shared future continuation branching: Distinguish continuations on std::experimental::shared_future from those on std::experimental::future: multiple continuations may attach to the shared state, each receives a shared_future, and each continuation's result is an ordinary future unless shared separately.
Why it matters: A shared result can safely feed more than one dependent operation.
Key terms: std::async, continuations, UI thread, std::experimental::future, .then(), .get(), exception, future-unwrapping, std::experimental::future<user_id>, async_authenticate_user
4.4.5 Waiting for more than one future
This section shows how to gather results from multiple asynchronous tasks without dedicating a thread to waiting for each future, and introduces waiting for the first result instead.
- Cost of individual future waits: Explain why gathering independently processed results by waiting on each future in a separate asynchronous task can be inefficient.
Why it matters: A waiting thread and repeated wakeups can add overhead even when the processing tasks run concurrently. - All-results future and continuation: Describe how std::experimental::when_all produces a future that is ready once every supplied future is ready, allowing a .then continuation to gather and process their results without blocking on each get call.
Why it matters: It lets follow-up work be scheduled when all results are available without keeping a gathering thread waiting. - First-ready future alternative: Distinguish std::experimental::when_any from when_all: its future becomes ready when any supplied future is ready, for work that should begin with the first available result.
Why it matters: Some follow-up work depends on the first result rather than on every task finishing.
Key terms: std::async, future, context switches, std::experimental::when_all, .then, get, when_any, when_all
4.4.6 Waiting for the first future in a set with when_any
This section shows how to use std::experimental::when_any to react to the first completed future and keep checking parallel search results until one succeeds or all fail.
- First ready future result: Explain that std::experimental::when_any returns a new future that becomes ready when at least one input future is ready. Describe how its when_any_result contains the collection of futures and an index identifying which one triggered readiness.
Why it matters: The index lets you inspect the completed task without waiting for every task to finish. - Repeated search result checks: Trace the parallel search example: tasks search separate chunks, and a continuation checks the future that became ready. If that task found no value, the continuation removes its future and calls when_any again; a found value sets the final result, while exhausting the futures stores an exception.
Why it matters: The first task to finish might not find a match, so the search must keep considering other completed tasks. - Future overloads and ownership: Distinguish the iterator-range overloads of when_any and when_all, whose results hold a vector of futures, from their variadic forms, whose results hold a tuple. State that these functions move futures supplied through a container and take parameters by value, so futures passed directly must be explicitly moved or supplied as temporaries.
Why it matters: These overload and ownership rules affect how you supply futures and read the combined result. - Future versus phase waiting: Distinguish waiting for a future result from waiting for a group of threads to reach a code point or collectively process a certain number of items; the latter may be better suited to a latch or barrier.
Why it matters: Choosing the synchronization facility depends on what event the threads need to wait for.
Key terms: std::experimental::when_any, std::experimental::when_any_result, index, continuation, DoneCheck, done_flag, final result, iterator-range overloads, variadic forms, vector
4.4.7 Latches and barriers in the Concurrency TS
This section distinguishes a one-shot latch from a reusable barrier by explaining how each lets threads wait for events or for one another.
- Latch counter and readiness: Explain that a latch becomes ready when its counter reaches zero and stays ready until destroyed. Describe how decrements can come from one thread, multiple threads, or a combination.
Why it matters: A latch lets threads wait for a series of events without requiring a particular thread to signal each event. - Barrier arrival cycles: Describe how each thread arrives at a barrier once per cycle and blocks until all participating threads have arrived. Explain that the threads are then released and can use the barrier again for the next cycle.
Why it matters: A barrier lets a group of threads repeatedly synchronize their progress.
Key terms: latch, counter, std::experimental::latch, barrier, cycle, arrive at the barrier
4.4.8 A basic latch type: std::experimental::latch
This section explains how a latch lets threads signal that required work is ready without requiring their tasks to finish, and how the latch makes their changes visible to a waiting thread.
- Latch counter and operations: Describe how to construct a std::experimental::latch with an initial event count, call count_down as events occur, and use wait or is_ready to wait for or check readiness. Explain that count_down_and_wait combines signaling an event with waiting.
Why it matters: These operations let you coordinate threads around a specific number of events. - Data readiness versus task completion: Trace the example in which async tasks count down the latch after generating their data but continue processing, while the main thread waits on the latch before processing that data. Distinguish this readiness point from completion of every task, which the future destructors ensure at the end of the function.
Why it matters: Waiting for usable results need not prevent workers from doing further work concurrently. - Latch synchronization and visibility: Explain why the main thread can safely process data written by other threads after waiting on the same latch: count_down synchronizes with wait, making the workers' changes visible when wait returns.
Why it matters: The latch establishes visibility of the generated data, not just a convenient notification. - Async lambda capture choices: Explain why the example captures the loop counter i by value while sharing data and done by reference: capturing i by reference would cause a data race and undefined behavior.
Why it matters: The capture choices keep each task's loop index separate while allowing the tasks to share the data and latch.
Key terms: std::experimental::latch, <experimental/latch>, count_down, wait, is_ready, count_down_and_wait, std::async, std::future, latch, process_data
4.4.9 std::experimental::barrier: a basic barrier
This section explains how a reusable barrier coordinates a fixed group of threads across processing stages and illustrates where to place barrier waits in a data-processing loop.
- Barrier cycles and membership: Explain how std::experimental::barrier releases a group of threads when all have arrived, then resets for another cycle. Distinguish it from a latch and describe what happens when a thread calls arrive_and_drop.
Why it matters: A barrier is useful when the same group must meet repeatedly before moving to the next stage. - Data processing synchronization points: Trace the example’s serial, parallel, and output stages, explaining why every thread calls arrive_and_wait before parallel processing and again before thread 0 writes the result.
Why it matters: The placement of each wait ensures that a stage does not begin before the work it depends on is complete. - Basic and flexible barriers: Distinguish the basic barrier from std::experimental::flex_barrier: the latter can run a final serial region after all threads arrive but before releasing them, with potentially more overhead.
Why it matters: The distinction explains why the basic barrier may suffice when no work is needed at the release point.
Key terms: std::experimental::barrier, arrive_and_wait, arrive_and_drop, synchronization group, latch, Listing 4.26, source, sink, joining_thread, std::experimental::flex_barrier
4.4.10 std::experimental::flex_barrier—std::experimental::barrier’s flexible friend
This section shows how std::experimental::flex_barrier runs serial work at a barrier and can change the number of participating threads between cycles.
- Barrier completion function: Explain that flex_barrier adds a constructor taking a thread count and a completion function, which runs on exactly one arriving thread after all participating threads have arrived.
Why it matters: The completion function provides a serial region without assigning that work to a particular thread. - Next-cycle thread count: Describe how the completion function's return value sets the required arrivals for the next barrier cycle: -1 keeps the count unchanged, while zero or more specifies a new count. Explain that the programmer must ensure the correct number of threads arrives next time.
Why it matters: Changing the count lets a barrier accommodate phases with different numbers of participating threads. - Simplified parallel loop: Trace how the example moves serial work into a flex_barrier completion function: an extracted lambda splits the first data block before work starts, and the completion function performs the old end-of-iteration work and splits the next block. Describe why the main loop then contains only parallel work and one synchronization point.
Why it matters: The rewrite shows how putting serial work at the barrier can simplify a parallel loop.
Key terms: std::experimental::flex_barrier, completion function, serial region, participating threads, next cycle, -1, split_source, synchronization point
4.5 Summary
The chapter summary explains why threads need synchronization, recaps the facilities and programming styles covered, and points ahead to the memory model and atomic operations.
Where this is used later
- (chapter intro) (chapter 8) · Waiting for another thread’s operation or condition.
- Interrupting a wait on std::condition_variable_any (chapter 9) · Distinguishes waits on std::condition_variable_any from waits on std::condition_variable.
- A thread-safe queue using locks and condition variables (chapter 6) · Uses locks and condition variables in a thread-safe queue.
- Interrupting a condition variable wait (chapter 9) · Provides the basis for reasoning about a condition-variable wait that can be interrupted.
- (chapter intro) (chapter 6) · Shows an earlier thread-safe queue using condition variables.
- A thread-safe stack using locks (chapter 6) · A thread-safe queue that incorporates condition-variable waiting
- A thread-safe queue using fine-grained locks and condition variables (chapter 6) · A thread-safe queue can wait for items using a condition variable.
- Ordering non-atomic operations (chapter 5) · Futures, promises, and background tasks
Concepts explained in this chapter
- futureAn object representing a value that may become available later, often as the result of asynchronous work.
- continuationA continuation is a function scheduled to run automatically after an asynchronous computation reaches a specified completion point.
- std::promise`std::promise` is a C++ standard library class template that stores a value or exception in a shared state for an associated future to retrieve.
- std::shared_futureA C++ Concurrency TS class template representing an asynchronous result accessible by multiple consumers.
- condition variableA condition variable is a synchronization primitive that lets threads wait for a condition and be notified when it may have changed.
- std::async`std::async` is a C++ standard library function template that launches a function asynchronously or defers its execution and returns a future for its result.
- std::packaged_task`std::packaged_task` is a C++ standard library class template that wraps a callable and stores its result in a shared state accessible through a future.
- functional, stateless philosophyFunctional programming is a programming style that emphasizes computing results from inputs while avoiding mutable external state.
- thread-safe queueA queue designed to support safe operations by multiple threads.
- message passingMessage passing is a communication technique in which concurrent components exchange information by sending and receiving messages.
- then()`then()` is a C++ Concurrency TS future operation that attaches a continuation to run after the future becomes ready.
- actor modelThe actor model is a concurrency model in which independent actors communicate by sending messages and do not share state directly.
- finite-state machineA finite state machine is a computational model with a finite set of states and transitions triggered by inputs or events.
- clockA C++ chrono clock is a class that provides a time source, a time-point type, a tick period, and information about whether it is steady.
- std::experimental::flex_barrier`std::experimental::flex_barrier` is an experimental C++ barrier synchronization primitive whose completion function can change the required participant count for a later cycle.
- steady clockA steady clock is a clock whose measured time never decreases and advances at a steady rate, making it suitable for measuring elapsed time.
- std::experimental::when_anyA C++ Concurrency TS function that returns a future which becomes ready when any supplied future is ready.
- time_point`std::chrono::time_point` is a C++ standard library class template representing a point in time relative to a clock's epoch.
- absolute timeoutAn absolute timeout is a deadline expressed as a particular point on a clock's timeline, rather than as a relative duration to wait.
- std::chrono::duration`std::chrono::milliseconds` is a C++ standard library duration type representing milliseconds.
- barrierA synchronization primitive that makes participating threads wait until all have reached a synchronization point.
- synchronizationSynchronization is the coordination of operations across threads so their effects are correctly ordered or observed.
- get`get()` is a future operation that retrieves the result represented by the future, waiting for it if necessary and rethrowing any stored exception.
- lambdaA lambda expression is a C++ expression that defines an unnamed callable, optionally capturing variables from its surrounding scope.
- spurious wakeupA spurious wakeup is a return from a condition-variable wait without a notification that makes the awaited condition true.
- mutable shared stateShared mutable data is information accessible by multiple threads that can also be modified during execution.
- separation of concernsSeparation of concerns is a software design principle that organizes a system so distinct concerns are handled independently.
- serial regionA portion of a program in which one thread performs the computation rather than multiple threads working concurrently.
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.
