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.
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).
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
-std=c++20 -pthread.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.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.
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)
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.
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
| Use | When |
|---|---|
std::atomic<T> | one simple value (counter, flag) updated by many threads |
std::mutex + guard | a compound operation or multiple values that must stay consistent together |
Source: cppreference — std::atomic.
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
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.count++ on a plain shared int with no synchronization. This is:std::scoped_lock rather than m.lock()/m.unlock()?std::thread before it's destroyed?join() it (wait) or detach() it — otherwise the program terminates. std::jthread (C++20) joins automatically.std::scoped_lock lock(m); — so only one thread enters the critical section and the lock releases automatically.atomic vs mutex?atomic<T> for one simple value (counter/flag), lock-free and fast. mutex for compound operations or several values that must stay consistent together.std::async and std::future give you?future; .get() waits for and returns its result — no manual threads or locks. The easiest safe parallelism.async/future > atomic > mutex > raw thread. Share less; protect what you share.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.