Concepts / Threads and Shared Data

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.

  • Programming

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?

  • Yes, always

  • No, but only when the pieces are independent and there is useful work to overlap

  • No, splitting always makes the program slower

Reveal answer
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

Definition

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.

GoalWhat it improvesTypical source of gainWhat must be true
ThroughputHow much work gets doneSeveral processors working on one computationEnough independent work to spread across processors
ResponsivenessWhether the program stays ready to actOne part keeps moving while another part waits for a device or another machineSomething 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.

Task ATask ATask BTask BTask CTask Cthenthen
How do the tasks sit in time when they run one after another versus when they overlap?

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.

Worked example

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?

  1. 1
    Start

    The counter holds 0.

  2. 2
    Interleaved reads

    Thread A reads 0. Before Thread A writes anything, Thread B also reads 0.

  3. 3
    First write

    Thread A computes 0 + 1 and writes 1 into the counter.

  4. 4
    Second 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.

Result

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.

Thread AThread BShared counterstarts at 0reads 0reads 0writes 1writes 1
Which reads and writes reach the shared counter, and in what order, when no access control is used?

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.

Key point

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.

  1. 1
    A thread performs a lock operation on the mutex that guards the shared data.
  2. 2
    While it holds that control, the thread reads the shared data, changes it, and writes it back.
  3. 3
    The thread performs a release through the mutex, giving up control so another thread can take it.
  4. 4
    A 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.

Higher-levelconcurrencylibrariesStandard libraryfacilitiesthreads, mutexes, lockoperations, packaged tasks,futuresOperating systemfacilitiesbuilt onbuilt on
Which layer is built on which, from the operating system up to higher-level libraries?

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 like

    Splitting a strictly sequential chain of steps, where each step needs the previous result, into several threads

    Why it fails

    Without independent work there is nothing to overlap, so coordination adds cost without adding progress

    Do this instead

    Check 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 like

    Two threads each adding 1 to a shared counter without any lock operation

    Why it fails

    The unprotected accesses can interleave, producing a data race and losing the guarantee of expected behavior

    Do this instead

    Control 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 fails

    The facilities are built directly on operating system capabilities and offer a portable, type-safe way to use them

    Do this instead

    Judge a design by its structure of independent work and waiting, not by the presence of a library feature

Practice: Deciding Whether Concurrency Helps

MEDIUM

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.

More concepts starting with T