C++ Standard Library · Lesson 13

Concurrency Essentials

Run work on multiple cores with portable standard tools — threads, the data-race problem, and the three ways to stay safe: mutex, atomic, and the easy async/future.

🎯 Use more than one core — without corrupting your data
The one idea

Since C++11 the library has portable threading. The hard part isn't starting threads — it's shared mutable data: two threads touching the same variable is a data race (undefined behavior). The whole toolkit exists to avoid that: mutex (lock it), atomic (lock-free for simple types), and async/future (don't share — return results).

01 std::thread — run a function elsewhere

A std::thread starts running a function immediately on (potentially) another core. You must then join() it (wait for it to finish) before it's destroyed — forgetting to is a crash.

#include <thread>

void work(int id) { /* ... */ }

std::thread t(work, 1);    // starts running work(1) now, in parallel
// ... do other things on this thread ...
t.join();                  // wait for it to finish (required before t dies)

// C++20: std::jthread joins automatically in its destructor (RAII) — prefer it
std::jthread jt(work, 2);   // no manual join needed
Run on godbolt.org with -std=c++20 -pthread.
⚠ A non-joined, non-detached thread crashes the program If a std::thread is destroyed while still "joinable," the program calls std::terminate. Always join() (wait) or detach() (let it run free) — or use std::jthread, which joins for you.

Source: cppreference — std::thread, std::jthread (C++20).

02 The enemy: data races

If two threads access the same memory and at least one writes, with no synchronization, you have a data race — undefined behavior. The result isn't just "sometimes wrong"; it can be arbitrarily broken. Even count++ is unsafe: it's really read-modify-write, and two threads can interleave.

Thread A: count++ Thread B: count++ shared count = 0 ends at 1, not 2! read 0 add 1 → 1 write 1 read 0 add 1 → 1 write 1
Both threads read 0, both write 1 — one increment is lost. The fixes below make the read-modify-write indivisible.

Source: cppreference — data races & the memory model.

03 mutex — lock the shared data

A std::mutex is a lock: only one thread can hold it at a time. Wrap it in a RAII guard (std::lock_guard or std::scoped_lock) so it unlocks automatically — even if the code throws.

#include <mutex>

std::mutex m;
int count = 0;

void bump() {
    std::scoped_lock lock(m);   // locks here...
    ++count;                     // critical section — only one thread at a time
}                                // ...unlocks here automatically (RAII)
💡 Never lock/unlock by hand Always take the lock via scoped_lock/lock_guard — manual m.lock()/m.unlock() leaks the lock on any early return or exception, deadlocking everything. RAII makes the unlock impossible to forget.

Source: cppreference — std::mutex, std::scoped_lock.

04 atomic — lock-free for simple values

For a single counter or flag, a full mutex is overkill. std::atomic<T> makes operations on a simple value indivisible without an explicit lock — fast and race-free.

#include <atomic>

std::atomic<int> count{0};
count++;                // atomic read-modify-write — safe from all threads
count.fetch_add(5);     // also atomic
int now = count.load();  // atomic read
UseWhen
std::atomic<T>one simple value (counter, flag) updated by many threads
std::mutex + guarda compound operation or multiple values that must stay consistent together

Source: cppreference — std::atomic.

05 async / future — the path of least pain

Often you don't need to share data at all — you want to run a computation elsewhere and collect its result. std::async runs a function (maybe on another thread) and hands back a std::future; calling .get() waits for and returns the result. No threads to join, no locks.

#include <future>

std::future<int> f = std::async(std::launch::async, []{
    return expensive_sum();      // runs in parallel
});

// ... do other work meanwhile ...

int result = f.get();          // waits if not done, then returns the value
🎯 The hierarchy to remember Prefer the highest-level tool that fits: async/future (return results, share nothing) > atomic (one simple shared value) > mutex (general shared state) > raw thread (full control). Share less; protect what you must share. This lesson is a map, not the territory — concurrency is deep, and the communities in RESOURCES.md are where the real wisdom lives.

Source: cppreference — std::async / std::future.

06 Check yourself

Two threads do count++ on a plain shared int with no synchronization. This is:
Why take a mutex via std::scoped_lock rather than m.lock()/m.unlock()?
You just need a counter incremented safely by many threads. Lightest correct tool?
You want a value computed in parallel and then to collect the result. Cleanest tool?

07 Flashcards

Q: What's a data race?
A: Two threads access the same memory with at least one writing, without synchronization — undefined behavior. The core hazard concurrency tools defend against.
Q: What must you do with a std::thread before it's destroyed?
A: join() it (wait) or detach() it — otherwise the program terminates. std::jthread (C++20) joins automatically.
Q: How do you protect shared data with a mutex?
A: Take it via a RAII guard — std::scoped_lock lock(m); — so only one thread enters the critical section and the lock releases automatically.
Q: atomic vs mutex?
A: atomic<T> for one simple value (counter/flag), lock-free and fast. mutex for compound operations or several values that must stay consistent together.
Q: What do std::async and std::future give you?
A: Run a function (possibly in parallel) and get a future; .get() waits for and returns its result — no manual threads or locks. The easiest safe parallelism.
Q: The tool hierarchy?
A: Prefer the highest-level fit: async/future > atomic > mutex > raw thread. Share less; protect what you share.
👩‍🏫 That's the course — well done

You've toured the everyday C++ standard library by category: containers, strings, iterators & algorithms, smart pointers, vocabulary types, I/O, and concurrency. From here: run the code on godbolt, keep the cheat sheet open, and bring me real code — "which container/algorithm/pointer fits here?" Tell me your concrete project or goal and I'll add targeted deep-dives to the mission.