Threads and Shared Data
The memory model defines what concurrent threads may rely on. Atomic operations are the second foundation. Together they let programmers build concurrent code on a defined base instead of guessing how the processor and compiler will reorder work.
Testing the Belief That Concurrency Makes Programs Faster
A common first reaction to hearing about concurrency is that it must make programs faster. The reasoning seems simple: split the work across several tasks that run at the same time, and the program should finish sooner or stay more responsive. This belief is partly right, which is what makes it persuasive, and partly wrong, which is what causes trouble. This article starts from that belief, tests it, corrects it, and then builds a clearer picture of what C++ offers for concurrent programming.
What do you think happens?
If one long task is split into several pieces that run at the same time, will the program always finish sooner?
Reveal answer
No, but only when the pieces are independent and there is useful work to overlap.
Concurrency is not a general speed setting. Throughput improves only when there is enough independent work to spread across processors, and responsiveness improves only when there is something useful to do while part of the program waits. If the work is one long chain of dependent steps, adding tasks adds coordination without adding progress.
The Two Goals Concurrency Serves
Concurrency is the execution of several tasks simultaneously. It is used to improve throughput, meaning how much work gets done, or to improve responsiveness, meaning that a program stays ready to act while part of it waits.
Throughput gains often come from using several processors on one computation. If a large job can be divided into pieces that do not depend on each other, each processor can take a piece and the total amount of completed work per unit of time can rise.
Responsiveness gains often come from a different arrangement. One part of a program keeps moving while another part waits for something outside itself, such as a response from a device or from another machine. The program as a whole stays ready to act instead of freezing during the wait.
| Goal | What it improves | Typical source of gain | What must be true |
|---|---|---|---|
| Throughput | How much work gets done | Several processors working on one computation | Enough independent work to spread across processors |
| Responsiveness | Whether the program stays ready to act | One part keeps moving while another part waits for a device or another machine | Something useful to do while a part of the program is waiting |
The two goals of concurrency differ in what they optimize and in what the program needs in order to benefit.
Notice what the two goals share. Neither one promises that every program will run faster. Each one depends on the shape of the work. Concurrency is therefore a tool suited to a specific kind of problem, not a setting that can be turned on for any program.
When Sequential Execution Is the Better Choice
The source is direct on this point: concurrency is not a panacea. If a task can be done sequentially, that is often simpler and faster. A sequential version has no shared bookkeeping, no waiting for other tasks to finish their part, and no need to coordinate access to data between them.
The diagram below compares the same three tasks run one after another and run overlapping in time. The sequential arrangement leaves no overlap at all, while the concurrent arrangement lets the tasks progress together. Whether the overlap pays off depends on whether the tasks are independent and whether the program would otherwise be idle.
Tracing Two Threads Through One Counter
A thread is a separate flow of execution, and threads inside one C++ program can see the same data. Consider a shared counter that starts at 0, where two threads each add 1 to it. The example below is a teaching illustration of how shared data can go wrong, not a claim about any particular program's output.
Two increments to one shared counter with no access control
Thread A and Thread B each read the counter, add 1, and write the result back. Neither thread takes control of the counter before accessing it. What final value can the counter end with?
- 1Start
The counter holds 0.
- 2Interleaved reads
Thread A reads 0. Before Thread A writes anything, Thread B also reads 0.
- 3First write
Thread A computes 0 + 1 and writes 1 into the counter.
- 4Second write
Thread B still holds the 0 it read earlier, computes 0 + 1, and writes 1. The earlier write of 1 is overwritten with the same value.
The counter ends at 1 instead of the expected 2. Both threads read and wrote the same variable without control, so the interleaving produced a wrong result. This is the situation the source calls a data race, in which the guarantee of expected behavior is lost.
Read the diagram in order from top to bottom. Each arrow is one access to the counter. Neither thread did anything incorrect in isolation; the problem lies in the unprotected overlap of their accesses to a shared value.
Controlled Access and the Memory Model
The most useful way to think about concurrency safety is to separate two situations. In the first, several threads access mutable data, but the access is controlled. The program then follows the rules of the C++ memory model and behaves as expected. In the second, the same data is accessed concurrently without control. That second situation is a data race, and the guarantee of expected behavior is lost.
The C++ standard library directly supports running several threads at once inside a single address space. Threads in one address space can see the same data, which makes them useful and also makes them dangerous. To make this workable, C++ supplies two foundations: a memory model that defines what concurrent threads may rely on, and a set of atomic operations.
The memory model defines what concurrent threads may rely on. Atomic operations are the second foundation. Together they let programmers build concurrent code on a defined base instead of guessing how the processor and compiler will reorder work.
How a Mutex Guards Shared Data
A mutex is a mechanism used to control access to shared data. Lock operations are how a thread takes that control and later releases it. Applied to the counter example, the fix is to make each read-add-write sequence happen under the mutex, so that only one thread at a time holds control of the counter.
- 1A thread performs a lock operation on the mutex that guards the shared data.
- 2While it holds that control, the thread reads the shared data, changes it, and writes it back.
- 3The thread performs a release through the mutex, giving up control so another thread can take it.
- 4A thread that needs the same data while another thread holds the mutex has to wait until the release happens before it can take control.
The final step is the one beginners often forget to picture. Waiting is the price of controlled access. The mutex prevents the interleaving from the counter example by forcing the two read-add-write sequences to take turns.
The Standard Library Facilities and What They Rest On
Most programmers meet concurrency through the standard library and through libraries built on top of it. The main facilities are threads, mutexes, lock operations, packaged tasks, and futures. A thread is a separate flow of execution. A mutex controls access to shared data, and lock operations are how a thread takes and releases that control.
A packaged task wraps work so that its result can be retrieved later, and a future is the handle through which that result is obtained. The source presents these as examples of the main facilities rather than a complete reference, so the useful habit is to learn what each one is for before memorizing its usage.
These facilities are built directly on what operating systems provide. The standard library does not add a hidden layer that makes them faster. It offers a portable and type-safe way to use the same underlying capabilities. Higher-level concurrency models are not supplied directly; they are built as libraries on top of these facilities, so the standard library supplies the building blocks while richer concurrency patterns form a layer above them.
Common Mistakes With Concurrent Code
Common mistake Assuming that more threads always means a faster program
What it looks likeSplitting a strictly sequential chain of steps, where each step needs the previous result, into several threads
Why it failsWithout independent work there is nothing to overlap, so coordination adds cost without adding progress
Do this insteadCheck whether the work is independent and whether useful work can happen while a part of the program waits before splitting it
Common mistake Reading and writing shared data with no access control
What it looks likeTwo threads each adding 1 to a shared counter without any lock operation
Why it failsThe unprotected accesses can interleave, producing a data race and losing the guarantee of expected behavior
Do this insteadControl access with a mutex and lock operations, and keep the memory model's rules in view
Common mistake Treating concurrency as a general-purpose speed setting instead of a tool for a specific problem shape
Common mistake Expecting the standard library facilities to add speed on their own
Why it failsThe facilities are built directly on operating system capabilities and offer a portable, type-safe way to use them
Do this insteadJudge a design by its structure of independent work and waiting, not by the presence of a library feature
Practice: Deciding Whether Concurrency Helps
For each workload below, decide whether concurrency is likely to help throughput, help responsiveness, or help neither, and explain your reasoning. (a) Ten independent image files must each be processed on a machine with several processors. (b) Each step of a calculation needs the result of the previous step. (c) A program must keep its interface ready to act while it waits for a reply from another machine.
Hints
- Ask whether the pieces of work depend on each other.
- Ask whether there is useful work the program can do while some part of it waits.
- A chain of dependent steps leaves nothing to overlap.
Key Takeaways
- Concurrency is the execution of several tasks simultaneously, used to improve throughput (how much work gets done) or responsiveness (staying ready to act while part of a program waits).
- Concurrency is not a universal fix: when work is a chain of dependent steps, sequential execution is often simpler and faster.
- When several threads access shared data without control, a data race occurs and expected behavior is lost; the C++ memory model's guarantees hold as long as data races are avoided.
- A mutex controls access to shared data, and lock operations let a thread take and release that control.
- The standard library's threads, mutexes, lock operations, packaged tasks, and futures are built directly on operating system facilities and offer a portable, type-safe way to use them rather than extra speed.