C++ PROGRAMMING ยท 13 MIN STUDY

C++ concurrency: threads, mutexes, and race conditions

Start safe concurrent programming with std::thread, shared data, mutex, lock_guard, and common race-condition mistakes.

Learning objectives

  • Start a thread and wait for it with join().
  • Explain why unsynchronized access to shared non-atomic data can create a data race and undefined behavior.
  • Protect shared state with std::mutex and an RAII lock such as std::lock_guard.
  • Identify the critical section and explain why the example reads the final value only after both threads finish.

Prerequisites: functions, object lifetime, and basic resource management. Review Functions, Scope and lifetime, and RAII first. Examples require C++11 or later.

Why concurrency?

A normal program often performs one sequence of work at a time. Concurrency means multiple tasks make progress during overlapping periods. A thread is an execution path within a process. Threads share the process's memory, which makes communication convenient but creates risks when they access the same data.

Start a thread

#include <thread>
#include <iostream>
void greet() { std::cout << "Hello from a thread\n"; }
int main() {
    std::thread worker(greet);
    worker.join(); // wait for completion
}

Build note: on many GCC and Clang Linux toolchains, compile this C++11 example with thread support enabled: g++ -std=c++11 -pthread main.cpp -o app. Other toolchains have their own thread-linking setup.

A joinable thread must be joined or detached before its thread object is destroyed. Prefer joining when the parent needs the result or must ensure work has finished. Detaching makes lifetime management harder and should not be a casual fix.

Race conditions and data races

If two threads update the same ordinary integer without synchronization, their operations can overlap. A data race on a non-atomic object is undefined behavior in C++. The final value is not merely unpredictable; the program has violated the language's rules.

int counter = 0;
void add() { for (int i = 0; i < 10000; ++i) ++counter; }
// Starting two add() threads without protection is unsafe.

Protect shared data with a mutex

#include <iostream>
#include <mutex>
#include <thread>

int counter = 0;
std::mutex counter_mutex;

void add() {
    std::lock_guard<std::mutex> lock(counter_mutex);
    for (int i = 0; i < 10000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread a(add);
    std::thread b(add);
    a.join();
    b.join();
    std::cout << "Counter: " << counter << '\n'; // always 20000
}

The mutex allows only one thread at a time to execute the protected loop. Each worker holds the lock for the whole batch, so the example avoids repeatedly locking for every increment. lock_guard uses RAII: it locks on construction and unlocks when it leaves scope, including during exception unwinding. After both joins, the main thread can safely read the final value.

Build note: this example uses C++11 threads. On many GCC and Clang Linux toolchains, compile with thread support enabled (for example, g++ -std=c++11 -pthread main.cpp -o app); follow your compiler's platform-specific instructions.

Important rules

  • Protect every access that participates in the same shared-data invariant, not only writes.
  • Keep critical sections short; do not hold locks while doing slow unrelated work.
  • Acquire multiple mutexes in a consistent order or use deadlock-avoiding facilities such as std::scoped_lock.
  • Do not destroy data while another thread may still access it.
  • Use atomics for suitable independent atomic state, not as a universal replacement for mutexes.

Practice

  1. Why must a joinable thread be joined or detached?
  2. What is the purpose of a mutex?
  3. Why is lock_guard safer than manually calling lock and unlock?
Show answers
  1. Destroying a joinable std::thread calls std::terminate.
  2. It serializes access to a protected critical section.
  3. RAII releases the lock automatically on every normal scope exit and during exception unwinding.

Summary

Threads enable concurrent work, but shared memory needs a synchronization design. Begin with explicit ownership, join threads, and protect shared invariants with RAII-based locks.

References