Chapter 20: Threads
Introduces threads as concurrent control flows through the B9 game, then shows how C starts, runs, and joins its task threads.
After this chapter you will be able to
- Concurrent task control flows. Explain how threads let a program pursue separate tasks concurrently, whether execution is simultaneous on multiple cores or interleaved on one.
- Thread creation and joining. Describe how the example uses thrd_create to start four task threads and has main join them after they terminate.
- B9 thread function pattern. Trace how B9 divides its work among four thread functions that receive shared game state and loop until they detect that the game has finished.
- Thread completion and shared signals. Distinguish waiting for another thread with thrd_join from communicating a condition through the shared finished member.
- Conflicting access and races. Explain why potentially simultaneous, unprotected accesses to a shared non-atomic object are dangerous, even when the object is a bool.
- Atomic operations across threads. Explain what _Atomic provides for shared members and how the section uses linearizability to reason about their operations across threads.
- Shared object initialization order. Explain when shared objects should be initialized relative to concurrent access, and when initialization by main, a creating thread, or call_once is appropriate.
- Once-only initialization guarantee. Describe how a shared once_flag and callback let call_once initialize an object exactly once before any using thread proceeds.
- Shared object destruction timing. Explain why destroying shared objects is harder to time than initializing them, and describe the section’s strategies for ending their lifetimes safely.
- Separation of thread data. Explain why separating the data threads access helps avoid race conditions and why function arguments and local variables are the preferred ways to hold thread-specific data.
- Thread local variable instances. Describe what thread_local does, where such variables may be declared, and the resulting limit on their initialization.
- Mutexes for critical data. Explain why the life example uses a mutex to protect game-board data rather than making the array atomic, and identify when its input thread needs that protection.
- Locking and linearizability. Describe what mtx_lock and mtx_unlock do for a critical section and what a thread can rely on after acquiring the same mutex another thread released.
- Lock ownership and balance. Explain the rules for relocking, unlocking, and releasing a mutex before its holding thread terminates.
- Condition checks after waiting. Explain why a thread checks its condition expression in a loop around a condition-variable wait, including when the game finishes.
- Waiting and signaling roles. Describe how a waiting thread uses the mutex with cnd_wait or cnd_timedwait, and how another thread uses cnd_signal or cnd_broadcast to wake waiters.
- Thread IDs and main. Explain why threads are not hierarchically organized and what makes main's thread special.
- Ending main without ending threads. Describe the precautions in the modified B9 example that let main's thread terminate without ending the other threads too early.
- Detaching unjoined threads. Justify detaching threads that will never be joined and describe where the example calls thrd_detach.
- Deadlock and livelock distinction. Distinguish threads stuck waiting on locks or conditions from threads that keep running in loops without meaningful progress.
- Consistent mutex locking order. Explain how two threads can deadlock when they acquire two mutexes in opposite orders, and state the section's rule for avoiding that situation.
- B9 thread progress argument. Trace why, if B9's two worker threads repeatedly reach life_wait, one of them can leave its loop and advance a counter.
- Visible cycles and termination. Explain why advancing internal counters alone might still look like livelock to a user, and how B9 addresses repeated cell configurations.
What it builds on
- (chapter intro) (chapter 19) · Introduces threads as another variation of control flow.
- Simple functions (chapter 7) · Assumes familiarity with functions that execute program work.
- Pointer operations (chapter 11) · Assumes familiarity with pointers used to refer to an object.
- Sequencing (chapter 19) · Provides the earlier notion of sequencing that the section extends to threads.
- Storage duration, lifetime, and visibility (chapter 13) · Storage duration and object lifetime.
- Deadlocks (chapter 15) · The earlier discussion identifies deadlock as a program failure.
Section by section
Introduction
Introduces threads as concurrent control flows through the B9 game, then shows how C starts, runs, and joins its task threads.
- Concurrent task control flows: Explain how threads let a program pursue separate tasks concurrently, whether execution is simultaneous on multiple cores or interleaved on one.
Why it matters: This explains how the game can keep progressing while waiting for player input. - Thread creation and joining: Describe how the example uses thrd_create to start four task threads and has main join them after they terminate.
Why it matters: Starting and joining threads defines how main launches the game's work and waits for it to finish. - B9 thread function pattern: Trace how B9 divides its work among four thread functions that receive shared game state and loop until they detect that the game has finished.
Why it matters: The example makes the abstract idea of separate thread control flows concrete without requiring every task to do the same work.
Key terms: threads, tasks, control flow, concurrently, <threads.h>, thrd_create, join, main, B9, Draw
20.1 Simple interthread control
This section explains how threads wait for one another and communicate through shared data, why conflicting accesses can cause races, and how atomic objects provide safe operations.
- Thread completion and shared signals: Distinguish waiting for another thread with thrd_join from communicating a condition through the shared finished member.
Why it matters: Waiting for completion and sharing a signal solve different coordination problems. - Conflicting access and races: Explain why potentially simultaneous, unprotected accesses to a shared non-atomic object are dangerous, even when the object is a bool.
Why it matters: A shared value is not safe merely because it is small or simple. - Atomic operations across threads: Explain what _Atomic provides for shared members and how the section uses linearizability to reason about their operations across threads.
Why it matters: Atomic operations make the shared signals in the example usable for interthread control. - Atomic declaration forms and limits: Describe the preferred _Atomic(T) declaration form, distinguish an atomic pointer from a pointer to atomic elements, and state the restriction on atomic array types.
Why it matters: Declaration syntax determines whether the pointer or its elements receive atomic operations.
Key terms: thrd_join, finished, shared, non-atomic object, race condition, _Atomic, atomic objects, indivisible, linearizable, _Atomic(T)
20.2 Race-free initialization and destruction
This section explains how to initialize shared objects before concurrent access, use call_once when initialization must happen later, and avoid destroying shared objects while threads may still use them.
- Shared object initialization order: Explain when shared objects should be initialized relative to concurrent access, and when initialization by main, a creating thread, or call_once is appropriate.
Why it matters: Initializing before sharing avoids exposing other threads to an unready object. - Once-only initialization guarantee: Describe how a shared once_flag and callback let call_once initialize an object exactly once before any using thread proceeds.
Why it matters: The guarantee prevents threads from using a partly initialized object or overwriting one another’s initialization. - Race-free stream output limits: Distinguish race-free use of an initialized FILE* from output that is readable as separate lines.
Why it matters: Race-free library calls alone do not guarantee readable log output. - Shared object destruction timing: Explain why destroying shared objects is harder to time than initializing them, and describe the section’s strategies for ending their lifetimes safely.
Why it matters: Premature destruction can cause a program to fail even after its main work is done.
Key terms: shared objects, static storage duration, automatic or allocated storage duration, call_once, once_flag, callback function, errlog, FILE*, race-free, fprintf
20.3 Thread-local data
The section explains how to avoid races by keeping thread-specific data separate, using local variables, thread_local variables, or thread-specific storage as appropriate.
- Separation of thread data: Explain why separating the data threads access helps avoid race conditions and why function arguments and local variables are the preferred ways to hold thread-specific data.
Why it matters: Separating data can avoid the need for more complex ways to coordinate threads. - Thread local variable instances: Describe what thread_local does, where such variables may be declared, and the resulting limit on their initialization.
Why it matters: It lets you choose thread_local when a per-thread variable does not need dynamic initialization. - Thread specific storage keys: Describe when tss_t is an alternative to thread_local and how its key and associated functions handle thread-specific data.
Why it matters: It identifies the option the section gives when thread_local cannot meet initialization and destruction needs.
Key terms: race conditions, thread-specific data, local variables, thread_local, static storage duration, file scope, thread-specific storage, tss_t, key, tss_dtor_t
20.4 Critical data and critical sections
This section explains how mutexes protect critical data, what their lock operations guarantee, and how to initialize, use, and destroy them correctly.
- Mutexes for critical data: Explain why the life example uses a mutex to protect game-board data rather than making the array atomic, and identify when its input thread needs that protection.
Why it matters: It explains why some shared data needs a protected critical section even when other shared objects are atomic. - Locking and linearizability: Describe what mtx_lock and mtx_unlock do for a critical section and what a thread can rely on after acquiring the same mutex another thread released.
Why it matters: The lock both excludes competing access and gives a thread a reliable view of earlier protected operations. - Mutex initialization and options: State how a C mutex must be initialized and distinguish the timed and recursive properties described here.
Why it matters: Choosing initialization properties determines which lock operations are valid for a mutex. - Lock ownership and balance: Explain the rules for relocking, unlocking, and releasing a mutex before its holding thread terminates.
Why it matters: Correct ownership and matching unlocks keep a critical section from remaining locked. - Mutex lifetime cleanup: Explain when mtx_destroy must be called and why mutex destruction matters.
Why it matters: Destroying a mutex releases resources that its initialization may have acquired.
Key terms: _Atomic, mtx_t, critical data, critical section, mtx_lock, mtx_unlock, linearizability, mtx_init, mtx_plain, mtx_timed
20.5 Communicating through condition variables
This section explains how threads use condition variables, condition checks, and a mutex to wait for work and signal one another safely.
- Condition checks after waiting: Explain why a thread checks its condition expression in a loop around a condition-variable wait, including when the game finishes.
Why it matters: Waking up tells a thread to check whether it can proceed, not that its work is ready. - Waiting and signaling roles: Describe how a waiting thread uses the mutex with cnd_wait or cnd_timedwait, and how another thread uses cnd_signal or cnd_broadcast to wake waiters.
Why it matters: Releasing and reacquiring the mutex lets another thread make progress while keeping critical data protected when the waiter resumes. - Condition variable mutex association: Explain the mutex-association rule for a condition variable and why several condition variables can share one mutex.
Why it matters: Different thread roles can wait for different work while protecting the same shared data. - Condition variable lifetime: State what must happen at the beginning and end of a cnd_t object's lifetime.
Why it matters: Condition variables need explicit lifetime management because they may tie up system resources.
Key terms: condition variable, condition expression, life_wait, cnd_timedwait, L->finished, mtx_t, cnd_wait, cnd_signal, cnd_broadcast, mutex
20.6 More sophisticated thread management
This section explains how to manage threads without relying on a parent-child hierarchy, including letting main end while other threads continue, detaching unjoined threads, and giving up processing time when a thread has no work.
- Thread IDs and main: Explain why threads are not hierarchically organized and what makes main's thread special.
Why it matters: Treating main like an ordinary thread exit could stop work that other threads are still doing. - Ending main without ending threads: Describe the precautions in the modified B9 example that let main's thread terminate without ending the other threads too early.
Why it matters: Ending main's thread safely also requires keeping shared data alive and arranging its cleanup. - Detaching unjoined threads: Justify detaching threads that will never be joined and describe where the example calls thrd_detach.
Why it matters: Detaching avoids retaining resources for threads the program does not intend to join. - Relinquishing processing time: Distinguish blocking, sleeping, and yielding as ways a thread can leave processing resources available to other threads.
Why it matters: A thread that cannot usefully run can make processor time available to other threads.
Key terms: thrd_t, main, exit, thrd_exit, L, global variable, atexit handler, thrd_detach, join, detached thread
20.7 Ensure liveness
This section distinguishes deadlock from livelock, gives rules for avoiding blocked waits, and sketches why example B9 continues to make progress or terminates.
- Deadlock and livelock distinction: Distinguish threads stuck waiting on locks or conditions from threads that keep running in loops without meaningful progress.
Why it matters: The distinction tells you whether a program has stopped because its threads are blocked or because they are running without advancing. - Consistent mutex locking order: Explain how two threads can deadlock when they acquire two mutexes in opposite orders, and state the section's rule for avoiding that situation.
Why it matters: A shared locking order prevents a common form of deadlock before it arises. - Timed condition waits in B9: Explain why the section recommends timed condition waits and how that recommendation applies to B9's deadlock argument.
Why it matters: B9's no-deadlock claim depends on considering condition waits as well as mutex locks. - B9 thread progress argument: Trace why, if B9's two worker threads repeatedly reach life_wait, one of them can leave its loop and advance a counter.
Why it matters: This is the section's concrete argument that B9's worker threads do not remain in an unproductive loop. - Visible cycles and termination: Explain why advancing internal counters alone might still look like livelock to a user, and how B9 addresses repeated cell configurations.
Why it matters: Thread activity is not enough if the visible game keeps returning to the same position.
Key terms: deadlock, livelock, mutexes, critical sections, same order, cnd_timedwait, cnd_wait, life_wait, mtx, account_thread
20.8 Summary
Where this is used later
- (chapter intro) (chapter 21) · The existence of concurrent threads.
- The “happened-before” relation (chapter 21) · Provides the context of controlling communication between threads.
- C library calls that provide synchronization (chapter 21) · The errlog initialization example.
Concepts explained in this chapter
- lockingA mutex is a mutual-exclusion synchronization object that allows only one thread at a time to hold a protected critical section.
- threadA thread of execution is an independently schedulable sequence of program operations within a running program.
- condition variableA condition variable is a thread-synchronization object that allows threads to wait until a shared-state condition may have become true.
- critical regionA critical section is a region of execution that accesses shared state under synchronization to prevent conflicting concurrent operations.
- cnd_timedwaitcnd_timedwait is a C threads-library function that atomically releases a mutex while waiting for a condition variable and returns when signaled or when an absolute time limit is reached.
- linearizabilityLinearizability is a concurrency correctness property under which each operation appears to take effect instantaneously at a point consistent with the ordering observed by threads.
- livenessLiveness is the property that a concurrent program continues to make progress rather than becoming permanently blocked or stalled.
- atomic typeA type specifies the kind of values an entity can represent and the operations applicable to those values.
- call_oncecall_once is a C threads-library function that invokes a callback at most once for a specified once-control object, coordinating concurrent callers.
- shared resourceA shared object is an object whose storage and value can be accessed by more than one thread.
- cnd_broadcastcnd_broadcast is a C threads-library function that wakes all threads waiting on a condition variable.
- cnd_signalcnd_signal is a C threads-library function that wakes one thread waiting on a condition variable.
- stopping criterionA condition is an expression whose evaluated value controls conditional or iterative execution in C.
- deadlockA deadlock is a condition in which multiple execution entities are permanently blocked because each depends on a resource or action held up by another in a cycle.
- livelockA livelock is a concurrency failure in which threads continue executing but repeatedly make no effective progress in response to one another.
- taskA task is a distinct part of a program's work that can be performed with little or no interaction with other tasks.
- thrd_createthrd_create is a C11 threads-library function that creates and starts a new thread with a specified start routine.
- threads.hthreads.h is the C standard header that declares the C11 thread-management and thread-synchronization interfaces.
- mtx_lockmtx_lock is a C threads-library function that blocks until the calling thread acquires a mutex.
- mtx_unlockmtx_unlock is a C threads-library function that releases a mutex previously acquired by the calling thread.
- race conditionA race condition is an execution condition in which the result depends on the relative timing or interleaving of accesses by concurrent or asynchronously executing code.
- scopeA scope is a region of a C program within which an identifier is visible and its declaration applies.
- storage classA storage class specifier is a C declaration specifier that controls an object's storage duration, linkage, or related storage properties.
- automatic objectAn automatic variable is a C object with automatic storage duration, normally created on entry to a block and destroyed on exit from it.
- cnd_waitcnd_wait is a C threads-library function that atomically releases a mutex while waiting on a condition variable and reacquires it before returning.
- function pointer typeA function pointer is a C pointer value that designates a function and can be used to invoke or pass that function indirectly.
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.
