C++ Concurrency: Threads, Mutexes, Futures and Atomics with Worked Examples
Learn what each C++ concurrency tool actually guarantees. Trace a lost update, repair it with a mutex and an atomic, and combine future results step by step.
KnowledgeGate Team
Exam prep & CS education

Creating a std::thread is easy. The hard part begins when two threads touch the same state without a clear ownership or synchronisation rule, because code that looks reasonable can become unreliable. An exact lost update starts from counter = 41; both a mutex and an atomic make the intended result 43 reliable, and two futures calculate the sum of squares from 1 through 6 as 91. Choose each tool according to the job instead of treating them as interchangeable features.
C++ concurrency: the four tools solve different problems
Concurrency is overlapping progress by multiple tasks. Parallelism is work executing simultaneously. A one-core program can be concurrent through interleaving, while parallel execution depends on the machine and scheduler. Never assume an order unless the program establishes it.
Tool | Main job |
|---|---|
| Run a callable |
| Give one thread exclusive access to a critical section |
| Represent a result that may arrive later |
| Provide indivisible operations on one supported object |
None makes a whole algorithm correct. A data race occurs when two threads perform conflicting operations on the same memory location, at least one is a write, and synchronisation does not order them. In C++, that is undefined behaviour, not another valid answer. Review the C++ Tutorial if lambdas, references or callables are unfamiliar.
std::thread: launch work, pass arguments and join before reading
Consider two independent multiplications:
void multiply(int x, int factor, int& out) {
out = x * factor;
}
int a = 0;
int b = 0;
std::thread t1(multiply, 7, 6, std::ref(a));
std::thread t2(multiply, 5, 9, std::ref(b));
t1.join();
t2.join();
std::cout << a + b;t1 writes a = 7 * 6 = 42, while t2 writes b = 5 * 9 = 45. After both join() calls, the main thread prints 42 + 45 = 87. Either may run first; the answer is unchanged because they write different objects.
Thread arguments are stored as decayed values, so std::ref is required here. Joining makes later reads safe. Destroying a joinable thread before join() or detach() calls std::terminate. Prefer owned arguments and joining because detached work can outlive referenced locals.
C++ race condition: an exact lost-update trace
Start with int counter = 41;. Two unsynchronised ++counter operations intend 41 + 1 + 1 = 43, but an illustrative read-modify-write interleaving is:
T1 reads
41, then T2 reads41.T1 computes
42, then T2 computes42.T1 writes
42, then T2 writes42.
The trace ends at 42, losing an increment. The actual C++ program has a data race and undefined behaviour, so 42 illustrates the hazard rather than guaranteeing an output. A race condition is timing dependence, a data race is the memory-model violation, and a critical section is protected code that must not overlap.

std::mutex: protect a multi-step critical section with RAII
A mutex repairs the counter by allowing only one thread into the update:
std::mutex m;
int counter = 41;
auto increment = [&] {
std::lock_guard<std::mutex> lock(m);
++counter;
};
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter;If T1 enters first, the values are 41 -> 42 -> 43. If T2 enters first, they are still 41 -> 42 -> 43. After both joins, the guaranteed output is 43.
std::lock_guard locks on construction and unlocks on destruction, even on early exit. Keep unrelated work outside the critical section. Opposite acquisition orders can deadlock. Use a consistent order or std::scoped_lock lock(m1, m2);. The wider model appears in Process Synchronization and Semaphores.
std::future and std::async: return values from concurrent work
A future lets each task deliver a value:
long long sum_squares(int first, int last) {
long long total = 0;
for (int x = first; x <= last; ++x) {
total += 1LL * x * x;
}
return total;
}
auto left = std::async(std::launch::async, sum_squares, 1, 3);
auto right = std::async(std::launch::async, sum_squares, 4, 6);
long long total = left.get() + right.get();The left result is 1^2 + 2^2 + 3^2 = 1 + 4 + 9 = 14. The right result is 4^2 + 5^2 + 6^2 = 16 + 25 + 36 = 77. Therefore, total = 14 + 77 = 91.
Explicit std::launch::async matters because the default policy may defer execution. get() waits, returns the value, and rethrows a captured exception. A future has single consumption, so a second get() is invalid. It transfers one result; mutexes and atomics coordinate shared state.

std::atomic: indivisible operations on one shared value
The counter can use one atomic operation per increment:
std::atomic<int> counter{41};
auto increment = [&] {
counter.fetch_add(1);
};
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter.load();Each fetch_add(1) is an atomic read-modify-write. Whichever thread goes first, the modification order is 41 -> 42 -> 43, and the value after both joins is 43.
Atomics suit one counter, flag or pointer, but do not combine objects into a transaction. If balance = 900 and transaction_count = 4 must become balance = 850 and transaction_count = 5 together, protect the pair with one mutex. This code uses sequential consistency. Consider memory_order_relaxed only when a counter guards no other data. Not every std::atomic<T> is lock-free.
C++ concurrency traps and representative test patterns
Trap | What goes wrong | Repair |
|---|---|---|
Destroying a joinable thread | Calls | Join or use scoped ownership |
Capturing a local by reference in longer-lived work | Reference dangles | Join in scope or move data |
Calling | Future is single-use | Store the first result |
Omitting | May defer execution | State the launch policy |
Treating | The data race remains | Use a mutex or atomic |
Updating two atomics as one invariant | A partial change is observable | Lock the invariant |
Representative tasks trace joined code, find a data race, choose lock_guard, compute a future, distinguish atomic increment from separate load and store, or spot opposite lock orders. Never demand an exact output from code with a data race.
Situation | Tool |
|---|---|
Independent task with no return value |
|
Shared multi-step invariant |
|
Asynchronous value or exception |
|
Single shared counter or flag | Consider |
C++ concurrency: the short version and next step
Keep this recall card:
Threads run callables, and you join before reading their results.
A data race is undefined behaviour.
Mutexes protect critical sections and multi-object invariants.
Futures carry eventual values and exceptions.
Atomics provide indivisible operations on one shared object. They are not automatically faster.
Reproduce the checkpoints without looking: 42 and 45 make 87; two safe increments move 41 to 43; 14 and 77 make 91. Then explain why the lost-update trace is illustrative, not a promised output.
For broader foundations and practice, continue with C++ Programming: Concepts, MCQs, Coding Questions. For a wider programming, DSA and placement route, explore Coding & DSA Courses for Placements. Choose from the ownership pattern, then code.
Keep learning

C++ Allocators and std::pmr: Build a Batch Arena for Short-Lived Data
Build a bounded C++20 batch arena for 1,500 short-lived strings, measure what the target library requests, and choose a PMR resource that fits the lifetime.

C++ Value Categories and Move Semantics: Trace What Actually Moves
Trace a unique allocation through lvalues, xvalues, prvalues, copy and move constructors, named rvalue references, const, and C++17 copy elision.

C++ Rule of Three, Five and Zero: Safe Object Copying and Moving
Follow one four-integer Buffer from a dangerous shallow copy to correct copy and move operations, then remove the lifetime code with std::vector.

C++ Object Lifetime and Storage Duration: Trace Construction, Destruction, and Dangling State
Learn to separate scope, storage duration, lifetime, and ownership. Trace seven objects precisely, then repair dangling pointers, references, and views.

