Chapter 9: Chapter 9. Advanced thread management
The introduction motivates automatic management of threads and tasks, explains why threads sometimes need to stop early, and previews the chapter’s two topics.
After this chapter you will be able to
- Automatic thread management motivation. Explain why creating a std::thread for every thread can be undesirable and describe the chapter’s aim of managing thread counts and dividing tasks automatically. Contrast this with cases where a standard library algorithm can handle parallelization.
- Early thread interruption reasons. Describe why threads working on a problem may need to finish early and what a stop request should let them do.
- Thread pool task flow. Explain why a thread pool shares worker threads rather than creating a thread for every potentially parallel task, and describe how submitted tasks are executed.
- Fixed worker pool and queue. Describe how a fixed set of worker threads processes submitted tasks from a thread-safe queue, including what workers do when the queue is empty.
- Safe pool shutdown and cleanup. Explain how the done flag and join_threads handle normal destruction and a failure while starting workers, and why member declaration order matters.
- Minimal pool task limitations. Distinguish what this pool supports from what it does not: submitted tasks have no returned value or built-in completion wait, so callers must arrange their own synchronization. Describe when this simplicity fits and why blocking or dependent tasks may call for another approach.
- Waiting for submitted tasks. Explain why callers wait for submitted tasks rather than pool worker threads, and how a future returned by submit() lets them wait for completion and obtain a result.
- Packaging move-only pool tasks. Trace how submit() deduces the callable’s return type, places it in a std::packaged_task, obtains its future, and moves the task into the queue. Explain why the queue needs a move-capable function wrapper instead of std::function<>.
- Parallel accumulation block size. Explain why parallel_accumulate divides work into blocks rather than assigning one block per thread, and how submission and future overhead limit how small those blocks should be.
- Dependent task deadlock. Explain how a fixed-size thread pool can deadlock when Quicksort tasks wait for other submitted tasks: every worker may be waiting while the tasks needed to finish remain queued.
- Pending work during waits. Describe how the pool's run_pending_task() method lets a waiting thread execute a queued task, and how the pool-based Quicksort uses it while waiting for a submitted chunk to finish.
- Shared queue contention. Explain why a single shared work queue can slow a thread pool as processor count grows: submitters push tasks while workers repeatedly pop them, causing contention and cache ping-pong even with a lock-free queue.
- Thread local queue operation. Describe how each pool worker gets a thread-local work queue, how submit() sends tasks to that queue only when called from a pool worker, and how run_pending_task() tries local work before the global queue.
- Local queue imbalance and stealing. Explain how local queues can leave one worker overloaded while others are idle, and state that idle workers can address this by stealing tasks from another worker after finding no work locally or globally.
- Stealable work queue design. Describe the work-stealing queue as a mutex-protected deque of tasks: its owner pushes and pops at the front, while other threads steal from the back.
- Pool-wide queue access and stealing. Trace how the pool makes every worker queue available for stealing: the constructor stores the queues and passes each worker its queue index; a worker running pending tasks tries its own queue, the pool queue, then other workers’ queues, starting its search after its own index.
- Cooperative thread shutdown. Explain that one thread signals another to stop before its work ends naturally, allowing the running thread to terminate gracefully rather than being stopped abruptly. Give a reason for doing so, such as pool destruction or user cancellation.
- Reusable interruption mechanism. Justify using a common interruption mechanism rather than designing one for each situation: it makes later code easier to write and allows interruptible code to be used without knowing its eventual context. State that C++11 does not provide this mechanism, so the section will build one starting with the launch-and-interrupt interface.
- Interruptible thread interface. Describe an interruptible_thread as providing a std::thread-like interface with an added interrupt() function, while code running in the thread calls a parameterless interruption_point() where it is willing to be interrupted.
- Thread-local flag handoff. Trace how the constructor wraps the supplied function, starts a std::thread, and uses a promise and future to obtain the address of that new thread’s thread_local interruption flag before returning.
- Safe interruption checkpoints. Explain that a thread must check its interruption flag, and that calling interruption_point() where interruption is safe throws thread_interrupted if the flag is set.
- Limits of explicit checkpoints. Explain why interruption_point() alone cannot handle a thread blocked while waiting: the thread is not running and cannot call it, so the wait needs to be interruptible.
- Lost notification race. Explain how an interruption between the initial interruption check and entering wait() can be missed because notification cannot wake a thread that is not yet waiting. Describe why protecting that gap with the wait’s mutex is rejected: the interrupting thread could deadlock on it or access it after destruction.
- Short timed wait alternative. Explain how repeated short wait_for() calls let the thread check for interruption without relying on a notification arriving after it starts waiting. Describe the bounded, clock-granularity-dependent delay and the trade-off of extra timeout wake-ups or predicate checks.
- Race-free interruptible wait sequence. Trace how the custom lock registers the condition variable, releases both locks during wait(), reacquires them on waking, and checks for interruption before clearing the registration.
- Timed waits for interruption. Explain why waits on a mutex or future generally cannot be interrupted directly, and describe how an interruptible_wait() overload for a future repeatedly uses wait_for() and checks the interrupt flag until the future is ready or interruption is requested.
- Catching and continuing after interruption. Explain that an interruption appears in the interrupted thread as a thread_interrupted exception that can be caught. Describe how a thread doing independent tasks can abandon the interrupted task and continue, while remaining interruptible on a later call to an interruption point.
- Wrapper-level interruption handling. Explain that letting thread_interrupted escape a std::thread entry function calls std::terminate() and terminates the program. Describe placing a catch for it in the interruptible_thread wrapper that initializes the interrupt_flag, so application work can let the exception propagate and only that thread terminates.
- Background task shutdown sequence. Trace the desktop search example: start filesystem-monitoring threads during initialization, let the main thread handle the GUI, and on exit interrupt every background thread before joining them.
- Interrupt before joining rationale. Explain why the main thread interrupts all background threads before joining any of them: an interrupted thread may still need to reach an interruption point and perform cleanup, so joining it immediately would delay interruption of the others.
What it builds on
- Choosing the number of threads at runtime (chapter 2) · Choosing an appropriate number of threads at runtime.
- A thread-safe queue using locks and condition variables (chapter 6) · The thread-safe queue used to hold pending work.
- Waiting in exceptional circumstances (chapter 2) · The need to join threads even when an exception occurs.
- Returning values from background tasks (chapter 4) · Background tasks that return values.
- Associating a task with a future (chapter 4) · associating a task with a future
- Dividing data recursively (chapter 8) · Dividing work recursively
- Data contention and cache ping-pong (chapter 8) · The performance cost of contention between threads
- Launching a thread (chapter 2) · Introduces launching a thread with std::thread.
Section by section
Introduction
The introduction motivates automatic management of threads and tasks, explains why threads sometimes need to stop early, and previews the chapter’s two topics.
- Automatic thread management motivation: Explain why creating a std::thread for every thread can be undesirable and describe the chapter’s aim of managing thread counts and dividing tasks automatically. Contrast this with cases where a standard library algorithm can handle parallelization.
Why it matters: This explains why the chapter turns from explicit thread creation to thread pools and task management. - Early thread interruption reasons: Describe why threads working on a problem may need to finish early and what a stop request should let them do.
Why it matters: An early result, an error, or a user cancellation should not leave unnecessary work running.
Key terms: std::thread, thread pools, tasks, standard library algorithm, interrupting threads, Please stop request
9.1 Thread pools
This section introduces thread pools as a way to reuse a limited set of threads for concurrent tasks and previews the design questions involved in building one.
- Thread pool task flow: Explain why a thread pool shares worker threads rather than creating a thread for every potentially parallel task, and describe how submitted tasks are executed.
Why it matters: This task flow is the foundation for the pool implementations that follow. - Thread pool design questions: State the design issues this introduction identifies: how many threads to use, how to allocate tasks to threads efficiently, and whether callers can wait for a task to finish.
Why it matters: These questions explain what the subsequent pool implementations are intended to address.
Key terms: thread pool, worker threads, queue of pending work, how many threads, allocate tasks to threads, wait for a task to complete
9.1.1 The simplest possible thread pool
This section builds a minimal fixed-size thread pool and explains how it runs tasks, shuts down safely, and where its limitations lie.
- Fixed worker pool and queue: Describe how a fixed set of worker threads processes submitted tasks from a thread-safe queue, including what workers do when the queue is empty.
Why it matters: The queue-and-worker loop is the foundation for the more capable pools that follow. - Safe pool shutdown and cleanup: Explain how the done flag and join_threads handle normal destruction and a failure while starting workers, and why member declaration order matters.
Why it matters: Workers must stop and be joined before the objects they use are destroyed. - Minimal pool task limitations: Distinguish what this pool supports from what it does not: submitted tasks have no returned value or built-in completion wait, so callers must arrange their own synchronization. Describe when this simplicity fits and why blocking or dependent tasks may call for another approach.
Why it matters: These limits motivate the later pool features and help avoid choosing a pool that can deadlock.
Key terms: worker threads, std::thread::hardware_concurrency(), thread-safe queue, submit(), std::function<void()>, std::this_thread::yield(), done flag, join_threads, try-catch block, worker_queue
9.1.2 Waiting for tasks submitted to a thread pool
The section makes thread-pool tasks waitable and result-bearing, then shows how to use them effectively in parallel_accumulate.
- Waiting for submitted tasks: Explain why callers wait for submitted tasks rather than pool worker threads, and how a future returned by submit() lets them wait for completion and obtain a result.
Why it matters: A pool’s workers can keep running after one task finishes, so the caller needs a handle to its particular task. - Packaging move-only pool tasks: Trace how submit() deduces the callable’s return type, places it in a std::packaged_task, obtains its future, and moves the task into the queue. Explain why the queue needs a move-capable function wrapper instead of std::function<>.
Why it matters: This is the implementation that lets one submission produce both queued work and a future for its result. - Parallel accumulation block size: Explain why parallel_accumulate divides work into blocks rather than assigning one block per thread, and how submission and future overhead limit how small those blocks should be.
Why it matters: Good block size lets the same code use a growing pool without making tiny tasks slower than serial work. - Task failures and dependency limits: Describe how task exceptions reach callers through futures and what the pool does with outstanding work if the calling function exits with an exception. Distinguish the independent tasks in this example from tasks that wait for other pool tasks.
Why it matters: The example’s convenience and exception behavior do not by themselves solve task dependencies.
Key terms: submit(), std::future<>, task handle, std::result_of<>, std::packaged_task<>, std::move(), function_wrapper, std::function<>, parallel_accumulate, num_blocks
9.1.3 Tasks that wait for other tasks
This section shows why recursive tasks can deadlock in a fixed-size thread pool and how running queued work while waiting makes a pool-based Quicksort possible.
- Dependent task deadlock: Explain how a fixed-size thread pool can deadlock when Quicksort tasks wait for other submitted tasks: every worker may be waiting while the tasks needed to finish remain queued.
Why it matters: A task cannot finish by waiting if all the workers that could run its dependency are also waiting. - Pending work during waits: Describe how the pool's run_pending_task() method lets a waiting thread execute a queued task, and how the pool-based Quicksort uses it while waiting for a submitted chunk to finish.
Why it matters: Doing useful work while waiting prevents the pool's workers from all being tied up in waits. - Shared queue contention: Explain why the revised pool is still not ideal: every submit() and run_pending_task() call accesses the same queue, which can hurt performance when many threads use it.
Why it matters: Avoiding deadlock does not by itself make a thread pool scale well.
Key terms: thread pool, tasks that wait for other tasks, deadlock, Quicksort, run_pending_task(), worker_thread(), do_sort(), thread_pool, submit(), queue
9.1.4 Avoiding contention on the work queue
This section reduces contention in a thread pool by giving workers local work queues, then explains why uneven workloads still require work stealing.
- Shared queue contention: Explain why a single shared work queue can slow a thread pool as processor count grows: submitters push tasks while workers repeatedly pop them, causing contention and cache ping-pong even with a lock-free queue.
Why it matters: Reducing traffic on the shared queue can improve thread-pool performance. - Thread local queue operation: Describe how each pool worker gets a thread-local work queue, how submit() sends tasks to that queue only when called from a pool worker, and how run_pending_task() tries local work before the global queue.
Why it matters: Local queues keep much of a worker’s task traffic off the shared queue. - Local queue imbalance and stealing: Explain how local queues can leave one worker overloaded while others are idle, and state that idle workers can address this by stealing tasks from another worker after finding no work locally or globally.
Why it matters: Work stealing preserves the contention benefit of local queues without letting uneven task distribution waste available workers.
Key terms: shared work queue, contention, cache ping-pong, lock-free queue, thread_local, std::unique_ptr<>, std::queue, submit(), run_pending_task(), worker_thread()
9.1.5 Work stealing
The section shows how a thread pool can let idle workers steal tasks from other workers’ queues using a simple, mutex-protected design.
- Stealable work queue design: Describe the work-stealing queue as a mutex-protected deque of tasks: its owner pushes and pops at the front, while other threads steal from the back.
Why it matters: The two ends let an idle worker take work without following the owner’s normal task order. - Owner and thief task order: Explain why the owner processes its most recently added task first, while a thief takes from the opposite end: recent tasks may have cache-local data, recursive sorting can finish a needed chunk sooner, and opposite-end access reduces contention.
Why it matters: Task order affects cache use, the number of active recursive tasks, and interference between workers. - Pool-wide queue access and stealing: Trace how the pool makes every worker queue available for stealing: the constructor stores the queues and passes each worker its queue index; a worker running pending tasks tries its own queue, the pool queue, then other workers’ queues, starting its search after its own index.
Why it matters: An idle worker cannot steal unless the pool can find other workers’ queues and decide when to search them.
Key terms: work_stealing_queue, std::deque<function_wrapper>, push(), try_pop(), try_steal(), mutex, last-in-first-out, cache, Quicksort, run_pending_task()
9.2 Interrupting threads
The section introduces cooperative thread interruption as a reusable way to ask a long-running thread to stop gracefully.
- Cooperative thread shutdown: Explain that one thread signals another to stop before its work ends naturally, allowing the running thread to terminate gracefully rather than being stopped abruptly. Give a reason for doing so, such as pool destruction or user cancellation.
Why it matters: A stop request lets a thread finish safely when its work is no longer needed. - Reusable interruption mechanism: Justify using a common interruption mechanism rather than designing one for each situation: it makes later code easier to write and allows interruptible code to be used without knowing its eventual context. State that C++11 does not provide this mechanism, so the section will build one starting with the launch-and-interrupt interface.
Why it matters: A common mechanism makes interruption useful across different tasks and applications.
Key terms: interrupting threads, signal, terminate nicely, common mechanism, C++11 Standard, interface for launching and interrupting a thread
9.2.1 Launching and interrupting another thread
The section sketches an interruptible thread interface and shows how its constructor gives the launching thread access to the new thread’s interruption flag.
- Interruptible thread interface: Describe an interruptible_thread as providing a std::thread-like interface with an added interrupt() function, while code running in the thread calls a parameterless interruption_point() where it is willing to be interrupted.
Why it matters: The interface separates a request to interrupt from the places where the running thread checks for that request. - Thread-local flag handoff: Trace how the constructor wraps the supplied function, starts a std::thread, and uses a promise and future to obtain the address of that new thread’s thread_local interruption flag before returning.
Why it matters: The handoff lets interruption_point() find the current thread’s flag without passing it through every function call. - Flag signaling and lifetime: Explain that interrupt() sets the flag when its pointer is valid, leaving the running thread to decide what to do, and identify the lifetime precautions the basic implementation still needs.
Why it matters: A valid flag pointer makes the request possible, but a stale pointer would make the design unsafe.
Key terms: interruptible_thread, std::thread, interrupt(), interruption_point(), thread_local, this_thread_interrupt_flag, promise, future, flag, lambda
9.2.2 Detecting that a thread has been interrupted
The section shows how a thread detects an interruption at a safe point and why that approach is insufficient while the thread is blocked.
- Safe interruption checkpoints: Explain that a thread must check its interruption flag, and that calling interruption_point() where interruption is safe throws thread_interrupted if the flag is set.
Why it matters: Setting an interruption flag has no effect unless the running thread checks it. - Limits of explicit checkpoints: Explain why interruption_point() alone cannot handle a thread blocked while waiting: the thread is not running and cannot call it, so the wait needs to be interruptible.
Why it matters: A blocked wait is often exactly where an application needs to interrupt a thread.
Key terms: interruption flag, interruption_point(), thread_interrupted, blocked waiting, interruptible
9.2.3 Interrupting a condition variable wait
The section shows why notifying a condition variable is not enough to interrupt its wait reliably, then uses short timed waits to bound how long an interruption can go unnoticed.
- Notification and interruption checks: Describe the proposed interruptible wait: check for interruption, associate the condition variable with the current thread’s interrupt flag, wait, clear the association, and check again. Explain why interrupting it requires notifying all waiters and why the association must be cleared even if the wait throws.
Why it matters: This first design exposes both the intended wake-up mechanism and an exception-safety problem. - Lost notification race: Explain how an interruption between the initial interruption check and entering wait() can be missed because notification cannot wake a thread that is not yet waiting. Describe why protecting that gap with the wait’s mutex is rejected: the interrupting thread could deadlock on it or access it after destruction.
Why it matters: The race explains why the otherwise straightforward notification-based design cannot be used reliably. - Short timed wait alternative: Explain how repeated short wait_for() calls let the thread check for interruption without relying on a notification arriving after it starts waiting. Describe the bounded, clock-granularity-dependent delay and the trade-off of extra timeout wake-ups or predicate checks.
Why it matters: Timed waits provide a usable interruption strategy despite the lost-notification race.
Key terms: interruptible_wait(), interruption_point(), interrupt_flag, std::condition_variable, spurious wake-ups, race condition, wait(), set_condition_variable(), mutex, deadlock
9.2.4 Interrupting a wait on std::condition_variable_any
The section shows how a custom lock makes waits on std::condition_variable_any interruptible without a race between registering the wait and entering it.
- Flexible condition variable locking: Explain why std::condition_variable_any makes this interruptible wait easier than std::condition_variable: it accepts a custom lock type that coordinates the caller’s lock with the interrupt flag’s internal mutex.
Why it matters: The ability to supply a custom lock is what makes the race-free coordination possible. - Race-free interruptible wait sequence: Trace how the custom lock registers the condition variable, releases both locks during wait(), reacquires them on waking, and checks for interruption before clearing the registration.
Why it matters: The order of locking and checking prevents an interrupt from being missed as the thread enters or leaves the wait.
Key terms: std::condition_variable_any, std::condition_variable, std::unique_lock<std::mutex>, custom lock type, set_clear_mutex, thread_cond_any, Lockable, custom_lock, interrupt flag, spurious wake
9.2.5 Interrupting other blocking calls
For blocking calls such as future waits that cannot be interrupted directly, this section uses short timed waits to check for interruption and explains the latency–overhead tradeoff.
- Timed waits for interruption: Explain why waits on a mutex or future generally cannot be interrupted directly, and describe how an interruptible_wait() overload for a future repeatedly uses wait_for() and checks the interrupt flag until the future is ready or interruption is requested.
Why it matters: Timed waits let a thread notice an interruption even when the blocking operation has no direct interruption mechanism. - Timeout latency and overhead: Describe how the timed-wait interval and clock resolution affect how quickly an interruption is noticed, and explain the cost of shortening the interval.
Why it matters: The timeout must balance prompt interruption against frequent wakeups and task-switching overhead.
Key terms: interruptible_wait(), std::future<>, wait_for, interrupt flag, 1 ms, high-resolution clock, clock tick, task-switching overhead
9.2.6 Handling interruptions
This section explains how an interrupted thread can handle a thread_interrupted exception, continue working, or terminate safely without ending the program.
- Catching and continuing after interruption: Explain that an interruption appears in the interrupted thread as a thread_interrupted exception that can be caught. Describe how a thread doing independent tasks can abandon the interrupted task and continue, while remaining interruptible on a later call to an interruption point.
Why it matters: Catching an interruption lets a worker stop one task without necessarily stopping the thread. - Exception safety during interruption: Explain why code that can be interrupted needs ordinary exception-safety precautions: an interruption can throw while work is in progress, so resources must not leak and data structures must remain coherent.
Why it matters: An interruption should not leave behind leaked resources or broken shared state. - Wrapper-level interruption handling: Explain that letting thread_interrupted escape a std::thread entry function calls std::terminate() and terminates the program. Describe placing a catch for it in the interruptible_thread wrapper that initializes the interrupt_flag, so application work can let the exception propagate and only that thread terminates.
Why it matters: A wrapper-level catch provides a single safe place to turn an unhandled interruption into termination of just the worker thread.
Key terms: thread_interrupted, catch block, interrupt(), interruption point, exception-safety precautions, resources, coherent state, std::terminate(), std::thread, interruptible_thread
9.2.7 Interrupting background tasks on application exit
The section shows how to shut down long-running background tasks by interrupting them, then joining them after all have been asked to stop.
- Background task shutdown sequence: Trace the desktop search example: start filesystem-monitoring threads during initialization, let the main thread handle the GUI, and on exit interrupt every background thread before joining them.
Why it matters: An orderly shutdown lets the application wait for its background work to finish before exiting. - Interrupt before joining rationale: Explain why the main thread interrupts all background threads before joining any of them: an interrupted thread may still need to reach an interruption point and perform cleanup, so joining it immediately would delay interruption of the others.
Why it matters: Issuing every interruption first allows the threads to handle shutdown concurrently instead of making them wait their turn.
Key terms: background threads, interrupt, join, interruption_point(), interruption point, destructor calls, exception-handling code
9.3 Summary
The chapter summary recaps how thread-pool scheduling can improve throughput and avoid deadlock, and how interruption points can let one thread stop another’s work.
Where this is used later
- Parallelizing the standard library algorithms (chapter 10) · Explores parallelization through standard library algorithms.
- (chapter intro) (chapter 10) · Covers thread pools as part of the preceding chapter’s advanced thread management.
Concepts explained in this chapter
- work stealingWork stealing is a scheduling technique in which an idle worker takes tasks from another worker’s queue.
- interruptible waitA blocking wait operation that returns control when the waiting thread is interrupted.
- thread poolA thread pool is a managed collection of reusable threads that execute submitted tasks.
- resource contentionThread contention is competition among threads for access to shared data, a resource, or a synchronization primitive.
- parallelismParallelism is the simultaneous execution of multiple tasks, typically to use available hardware and improve performance.
- std::thread::hardware_concurrencyA C++ standard library function that returns a hint about the number of hardware threads available for concurrent 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.
