Chapter 2: Chapter 2. Managing threads
The chapter introduces std::thread as the main way to manage threads and previews the thread-management tasks it will cover.
After this chapter you will be able to
- Thread Management Through std::thread. Explain that most C++ Standard Library thread-management tasks are handled through the std::thread object associated with a thread, beginning with launching it and deciding whether to wait for it or leave it running in the background.
- Thread entry points and exits. Explain that the C++ runtime starts a thread running main(), and that a program can launch additional threads with other functions as entry points. Describe how those threads run concurrently and exit when their entry-point functions return.
- Launching before waiting. State that a std::thread object can be used to wait for its thread to finish, but the thread must first be launched.
- Thread launch and callable copies. Explain that constructing a std::thread with a callable starts a new thread that runs the callable until it returns. Describe how a function object supplied to the constructor is copied into the new thread and must behave as expected when copied.
- Join or detach decision. Explain that after starting a thread, you must join it or detach it before its std::thread object is destroyed, including when exceptions occur. Distinguish the lifetime of the thread object from the lifetime of the running thread.
- Local data lifetime across threads. Explain why a thread that retains a pointer or reference to a local variable can access destroyed data if the launching function returns first. Describe the section's remedies: copy the needed data into a self-contained thread task or join the thread before the function exits.
- Joining and thread lifetime. Explain that calling join() on a std::thread waits until its thread finishes, so joining before leaving a function can keep local variables alive until the thread is done using them.
- Joining on every exit path. Explain why an exception between starting a thread and calling join() is dangerous, and describe how the try/catch approach ensures the thread finishes before the function exits.
- RAII thread guard behavior. Describe how a thread_guard uses RAII to join a thread in its destructor, including why destruction order protects local state and why it checks joinable() and forbids copying.
- Detached thread ownership and control. Explain that detach() leaves a thread running in the background while the std::thread object ceases to refer to it. The thread can no longer be joined, and the C++ Runtime Library reclaims its resources when it exits.
- Independent document editing threads. Describe the word-processor example: opening a document starts a thread running edit_document with the chosen filename, then detaches it because the current document's thread need not wait for the unrelated document. Relate this to long-running background or fire-and-forget uses of detached threads.
- Default argument copying. Explain that std::thread copies supplied arguments into internal storage and passes them to the callable as rvalues, without first converting them to the callable’s parameter types.
- Local buffer lifetime hazard. Explain why passing a pointer to a local character buffer can cause undefined behavior, and how constructing a std::string before launching the thread avoids it.
- Explicit reference argument passing. Explain why a callable expecting a non-const reference cannot receive a normally supplied thread argument, and how std::ref makes it receive the original object.
- Move only ownership transfer. Explain how std::move transfers a named move-only argument such as std::unique_ptr into a thread, and state that std::thread itself is movable but not copyable because it owns a thread of execution.
- Thread ownership moves. Trace how moving a std::thread transfers its associated thread of execution to another std::thread object, leaving the source without one. Distinguish an explicit move from a named object from the implicit move of a temporary.
- Occupied thread assignment hazard. Explain why move-assigning into a std::thread that already owns a thread calls std::terminate(), rather than silently discarding the thread it owned.
- Function and scope ownership. Describe how a function can return a std::thread to pass ownership out or accept one by value to take ownership in. Explain how scoped_thread takes ownership and joins on destruction, avoiding the lifetime problem of a guard that only references a thread.
- Runtime thread count. Explain how the example chooses a thread count from the range length, a minimum block size, and std::thread::hardware_concurrency(), including a fallback when the hardware hint is zero.
- Block division and execution. Trace how parallel_accumulate divides a nonempty range into blocks, launches worker threads, and uses the calling thread to process the final block.
- Intermediate result collection. Describe how each block stores its result, how the calling thread waits for workers, and how their results become the final accumulated value.
- Thread ID retrieval. Describe how to obtain a thread ID from a std::thread object or from the currently executing thread, and what get_id() returns when a std::thread has no associated thread.
- Thread-specific decisions and data. Describe how saved thread IDs can distinguish the initial thread or determine which operations a thread may perform, and how IDs can associate data with particular threads.
What it builds on
- Hello, Concurrent World (chapter 1) · Introduces starting a thread with std::thread.
Section by section
Introduction
The chapter introduces std::thread as the main way to manage threads and previews the thread-management tasks it will cover.
- Thread Management Through std::thread: Explain that most C++ Standard Library thread-management tasks are handled through the std::thread object associated with a thread, beginning with launching it and deciding whether to wait for it or leave it running in the background.
Why it matters: This gives you a framework for the thread operations explained next. - Other Thread Management Topics: Describe the chapter's remaining topics: passing parameters to a thread function at launch, transferring ownership between std::thread objects, choosing a thread count, and identifying threads.
Why it matters: This roadmap shows which questions about managing threads the chapter will address.
Key terms: std::thread, launching a thread, waiting for a thread to finish, running threads in the background, parameters, thread function, ownership, number of threads, identifying threads
2.1 Basic thread management
Introduces the main thread, the lifecycle of additional threads, and the need to launch a thread before waiting for it.
- Thread entry points and exits: Explain that the C++ runtime starts a thread running main(), and that a program can launch additional threads with other functions as entry points. Describe how those threads run concurrently and exit when their entry-point functions return.
Why it matters: This gives you the basic model for what a launched thread runs and when it finishes. - Launching before waiting: State that a std::thread object can be used to wait for its thread to finish, but the thread must first be launched.
Why it matters: Launching and waiting are separate steps in managing a thread.
Key terms: thread, main(), entry point, std::thread, wait
2.1.1 Launching a thread
This section shows how to launch a C++ thread and why its ownership and access to local data must be handled before the launching function exits.
- Thread launch and callable copies: Explain that constructing a std::thread with a callable starts a new thread that runs the callable until it returns. Describe how a function object supplied to the constructor is copied into the new thread and must behave as expected when copied.
Why it matters: The callable you give std::thread is the work the new thread actually runs. - Ambiguous temporary callable syntax: Explain how constructing std::thread with a temporary function object can be parsed as a function declaration rather than a thread object, and describe a syntax that avoids the ambiguity.
Why it matters: An ambiguous declaration can compile without launching the thread you intended. - Join or detach decision: Explain that after starting a thread, you must join it or detach it before its std::thread object is destroyed, including when exceptions occur. Distinguish the lifetime of the thread object from the lifetime of the running thread.
Why it matters: Leaving a thread object joinable at destruction terminates the program. - Local data lifetime across threads: Explain why a thread that retains a pointer or reference to a local variable can access destroyed data if the launching function returns first. Describe the section's remedies: copy the needed data into a self-contained thread task or join the thread before the function exits.
Why it matters: A detached thread must not keep using local data after the function that owns it has returned.
Key terms: std::thread, <thread>, callable, function object, function call operator, most vexing parse, uniform initialization, lambda expression, join, detach
2.1.2 Waiting for a thread to complete
This section explains how joining a std::thread waits for its completion, what happens to the thread object afterward, and when other waiting mechanisms are needed.
- Joining and thread lifetime: Explain that calling join() on a std::thread waits until its thread finishes, so joining before leaving a function can keep local variables alive until the thread is done using them.
Why it matters: Waiting before local variables are destroyed can prevent a thread from using objects that no longer exist. - Join limitations and aftermath: Describe join() as an all-or-nothing wait rather than a way to check completion or wait for a limited time. State that it cleans up thread-associated storage, leaves the std::thread object unassociated with a thread, and can be called only once on that thread.
Why it matters: These limits tell you when to use another waiting mechanism and when a thread object is no longer joinable.
Key terms: std::thread, join(), local variables, joinable(), condition variables, futures
2.1.3 Waiting in exceptional circumstances
This section shows how to ensure a thread is joined on both normal and exceptional exits, and how an RAII guard makes that guarantee less error-prone.
- Joining on every exit path: Explain why an exception between starting a thread and calling join() is dangerous, and describe how the try/catch approach ensures the thread finishes before the function exits.
Why it matters: A skipped join can leave a joinable thread object to be destroyed and terminate the application. - RAII thread guard behavior: Describe how a thread_guard uses RAII to join a thread in its destructor, including why destruction order protects local state and why it checks joinable() and forbids copying.
Why it matters: The guard makes joining happen during ordinary or exceptional scope exit without repeating verbose try/catch code. - Detachment as an alternative: Distinguish detaching a thread from joining it: explain when immediate detach avoids the exception-safety problem and what it means for the running thread.
Why it matters: Detachment avoids a missed join only when the program does not need to wait for that thread.
Key terms: join(), std::thread, try/catch, std::terminate(), RAII, thread_guard, destructor, joinable(), =delete, detach()
2.1.4 Running threads in the background
This subsection explains how detaching a std::thread lets it run independently, when detachment is valid, and why a document-editing application might use it.
- Detached thread ownership and control: Explain that detach() leaves a thread running in the background while the std::thread object ceases to refer to it. The thread can no longer be joined, and the C++ Runtime Library reclaims its resources when it exits.
Why it matters: Detaching changes who manages the thread and removes your ability to wait for it through std::thread. - Valid detachment condition: State that detach() can be called only when the std::thread object has an associated thread, which can be checked with joinable().
Why it matters: Checking joinable() prevents an attempt to detach a thread that the object does not hold. - Independent document editing threads: Describe the word-processor example: opening a document starts a thread running edit_document with the chosen filename, then detaches it because the current document's thread need not wait for the unrelated document. Relate this to long-running background or fire-and-forget uses of detached threads.
Why it matters: The example shows when giving up the ability to join a thread is a deliberate choice.
Key terms: detach(), std::thread, joinable, C++ Runtime Library, joinable(), associated thread of execution, daemon threads, fire-and-forget, edit_document, std::thread constructor
2.2 Passing arguments to a thread function
This section explains how std::thread passes arguments to a new thread, including copies, references, member functions, and move-only values.
- Default argument copying: Explain that std::thread copies supplied arguments into internal storage and passes them to the callable as rvalues, without first converting them to the callable’s parameter types.
Why it matters: The timing of a conversion can determine whether a thread receives valid data. - Local buffer lifetime hazard: Explain why passing a pointer to a local character buffer can cause undefined behavior, and how constructing a std::string before launching the thread avoids it.
Why it matters: A copied pointer does not keep the local data it points to alive. - Explicit reference argument passing: Explain why a callable expecting a non-const reference cannot receive a normally supplied thread argument, and how std::ref makes it receive the original object.
Why it matters: Explicit reference passing lets a thread update the intended object rather than attempt to use a temporary. - Member function invocation arguments: Describe how to invoke a member function in a new thread by supplying its member-function pointer followed by an object pointer, with any member-function arguments after that.
Why it matters: The same constructor can launch work belonging to a particular object. - Move only ownership transfer: Explain how std::move transfers a named move-only argument such as std::unique_ptr into a thread, and state that std::thread itself is movable but not copyable because it owns a thread of execution.
Why it matters: Move semantics allow ownership to pass into thread work and between thread objects without creating duplicate owners.
Key terms: std::thread constructor, internal storage, rvalues, implicit conversion, buffer, std::string, undefined behavior, non-const reference, std::ref, rvalue
2.3 Transferring ownership of a thread
This section shows how moving std::thread objects transfers responsibility for a thread, enabling functions, scope-bound wrappers, and containers to manage when threads finish.
- Thread ownership moves: Trace how moving a std::thread transfers its associated thread of execution to another std::thread object, leaving the source without one. Distinguish an explicit move from a named object from the implicit move of a temporary.
Why it matters: Tracking the current owner tells you which object is responsible for joining or detaching a thread. - Occupied thread assignment hazard: Explain why move-assigning into a std::thread that already owns a thread calls std::terminate(), rather than silently discarding the thread it owned.
Why it matters: An assignment that appears to replace a thread handle can instead terminate the program. - Function and scope ownership: Describe how a function can return a std::thread to pass ownership out or accept one by value to take ownership in. Explain how scoped_thread takes ownership and joins on destruction, avoiding the lifetime problem of a guard that only references a thread.
Why it matters: Transferring ownership lets code make responsibility for finishing a thread explicit and scope-bound. - Grouped thread completion: Describe how a move-aware std::vector<std::thread> can hold several spawned threads that are then joined as a group. Explain that the example's work communicates through effects on shared data, so a return value dependent on that work would have to be determined from the shared data after the threads finish.
Why it matters: Grouping thread owners simplifies management but does not, by itself, provide a way to return each thread's result.
Key terms: std::thread, std::move, ownership, temporary, move assignment, std::terminate(), join, detach, scoped_thread, thread_guard
2.4 Choosing the number of threads at runtime
The section uses a naïve parallel accumulate to show how to choose a thread count at runtime, divide work, collect results, and recognize the limits of the approach.
- Runtime thread count: Explain how the example chooses a thread count from the range length, a minimum block size, and std::thread::hardware_concurrency(), including a fallback when the hardware hint is zero.
Why it matters: Thread count must reflect both the available work and the machine's capacity. - Block division and execution: Trace how parallel_accumulate divides a nonempty range into blocks, launches worker threads, and uses the calling thread to process the final block.
Why it matters: The final block lets the example cover the whole range without requiring equal-sized divisions. - Intermediate result collection: Describe how each block stores its result, how the calling thread waits for workers, and how their results become the final accumulated value.
Why it matters: A worker thread cannot return its calculated value directly to the thread that launched it. - Parallel accumulation trade-offs: Explain the example's limitations: changed grouping can change results for nonassociative addition, and parallelization imposes stronger type and iterator requirements than std::accumulate.
Why it matters: Parallelizing an algorithm can change both its output and which inputs it accepts. - Thread identification need: Describe why passing an identifying number into a thread is sometimes inconvenient and state that each thread has a unique identifier.
Why it matters: Code deep in a call stack may need to identify its current thread without receiving an identifier through every call.
Key terms: std::thread::hardware_concurrency(), minimum block size, oversubscription, parallel_accumulate, block, std::thread, std::vector<T>, std::vector<std::thread>, std::for_each, std::accumulate
2.5 Identifying threads
This section explains how to obtain, compare, and use C++ thread identifiers, while treating their values as opaque.
- Thread ID retrieval: Describe how to obtain a thread ID from a std::thread object or from the currently executing thread, and what get_id() returns when a std::thread has no associated thread.
Why it matters: These are the two ways to find out which thread an ID identifies. - Thread ID comparisons and keys: Explain what equality and inequality mean for thread IDs, and how ordering and hashing let IDs serve as container keys.
Why it matters: Comparison and hashing make IDs useful for looking up information associated with a thread. - Thread-specific decisions and data: Describe how saved thread IDs can distinguish the initial thread or determine which operations a thread may perform, and how IDs can associate data with particular threads.
Why it matters: An ID lets shared code or a data structure recognize which thread is performing an operation. - Opaque ID values and output: Explain why thread IDs are generally sufficient as identifiers but should not be treated as meaningful values, including what streamed output guarantees.
Why it matters: Treating IDs as opaque avoids relying on implementation-dependent values.
Key terms: std::thread::id, get_id(), std::this_thread::get_id(), default-constructed std::thread::id, comparison operators, total ordering, std::hash<std::thread::id>, associative containers, unordered associative containers, thread-local storage
2.6 Summary
The chapter recap brings together the basics of managing C++ threads and explains why later chapters turn to shared data and synchronization.
Where this is used later
- Waiting for a condition with condition variables (chapter 4) · Condition variables provide an alternative to simply waiting for a thread to finish.
- Functions that accept timeouts (chapter 4) · Timeout-capable operations address the need to wait for only a limited time.
- Exception safety in parallel algorithms (chapter 8) · Waiting for threads to complete in exceptional circumstances
- The simplest possible thread pool (chapter 9) · The need to join threads even when an exception occurs.
- Race conditions (chapter 11) · Waiting for a thread to finish even in exceptional circumstances.
- Returning values from background tasks (chapter 4) · Introduces passing arguments to thread functions.
- Oversubscription and excessive task switching (chapter 8) · Explains the performance cost of oversubscribing processors.
- Dividing data between threads before processing begins (chapter 8) · Provides the earlier accumulate example whose results are combined after worker threads finish.
Concepts explained in this chapter
- std::threadA C++ standard library class that represents and manages a thread of execution.
- std::move`std::move` is a C++ standard library function template that casts an expression to an rvalue reference, enabling move operations.
- std::thread::detach()`std::thread::detach()` separates a thread of execution from its `std::thread` object, allowing it to continue independently.
- callableA callable object is an object that can be invoked with function-call syntax.
- lambdaA lambda expression is a C++ expression that defines an unnamed callable, optionally capturing variables from its surrounding scope.
- object lifetimeAn object's lifetime is the period during which the C++ object exists and may be validly accessed.
- moveMove semantics are C++ language features that allow resources to be transferred from one object to another without copying them.
- ownership transferOwnership transfer is the reassignment of responsibility for managing a resource, such as a lock, from one entity to another.
- hardware concurrencyHardware concurrency is the degree of parallel execution supported by the available hardware.
- std::thread::hardware_concurrencyA C++ standard library function that returns a hint about the number of hardware threads available for concurrent execution.
- thread argument passingThread argument passing is the behavior governing how arguments are stored and supplied when starting a thread function.
- resource acquisition is initializationResource Acquisition Is Initialization is a C++ programming idiom that binds resource ownership and release to object lifetime.
- threadA unit of execution within a process that can run independently of other threads.
- oversubscriptionA condition in which more threads are run than the hardware can support concurrently.
- undefined behaviorUndefined behavior is program behavior for which the C++ language standard imposes no requirements.
- exception safetyException safety is the property of a program or operation maintaining defined behavior and resource integrity when exceptions occur.
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.
