Threads and Concurrency
Modern computers have many cores, and C++ lets a program use them: std::thread runs functions in parallel, mutexes protect shared data, and std::async runs a task and gives you its result later. Concurrency is powerful but subtle — this lesson covers the safe essentials.
Starting threads
A std::thread starts running a function as soon as it's created. Before the std::thread object is destroyed you must join it — wait for it to finish — or the program terminates:
#include <iostream> #include <thread> #include <vector> int main() { std::vector<long long> results(4); std::vector<std::thread> workers; for (int t = 0; t < 4; ++t) { workers.emplace_back([t, &results] { long long sum = 0; for (long long i = t * 250'000; i < (t + 1) * 250'000; ++i) sum += i; results[t] = sum; // each thread writes only its own slot }); } for (auto& w : workers) w.join(); // wait for all four long long total = 0; for (auto r : results) total += r; std::cout << "sum of 0..999999 = " << total << '\n'; }
sum of 0..999999 = 499999500000
C++20 adds std::jthread, which joins automatically in its destructor (RAII again) and supports cooperative cancellation. Prefer it where available — GCC's and Microsoft's libraries have it, but Apple's libc++ still treats it as experimental, so this lesson's examples use std::thread.
Data races
When two threads access the same variable at the same time and at least one writes, that's a data race — undefined behaviour. Even ++counter is a read, an add and a write, and two threads can interleave them and lose updates:
int counter = 0; // four threads, each running: for (int i = 0; i < 100'000; ++i) ++counter;
Five runs of this program on the test machine printed totals between 113,156 and 126,423 — never the expected 400,000, and different every time.
ThreadSanitizer (-fsanitize=thread) detects data races while testing.
Mutexes
A std::mutex lets only one thread at a time into a critical section. Lock it with an RAII guard — std::lock_guard or std::scoped_lock — so it's always unlocked, even if an exception is thrown:
#include <iostream> #include <mutex> #include <thread> #include <vector> int main() { int counter = 0; std::mutex m; std::vector<std::thread> threads; for (int t = 0; t < 4; ++t) { threads.emplace_back([&] { for (int i = 0; i < 100'000; ++i) { std::lock_guard lock{m}; ++counter; } }); } for (auto& th : threads) th.join(); std::cout << "counter = " << counter << '\n'; }
counter = 400000
Keep critical sections short. When code locks several mutexes, std::scoped_lock locks them all at once without risk of deadlock.
Atomics
For a single counter or flag, std::atomic is simpler and faster than a mutex: its operations are indivisible.
#include <atomic> #include <iostream> #include <thread> #include <vector> int main() { std::atomic<int> counter{0}; std::vector<std::thread> threads; for (int t = 0; t < 4; ++t) { threads.emplace_back([&counter] { for (int i = 0; i < 100'000; ++i) ++counter; }); } for (auto& th : threads) th.join(); std::cout << "counter = " << counter << '\n'; }
counter = 400000
std::async and futures
std::async runs a function — possibly on another thread — and returns a std::future from which you get() the result, waiting if necessary. Exceptions thrown in the task are rethrown by get():
#include <future> #include <iostream> #include <numeric> #include <vector> long long sum_range(const std::vector<int>& v, std::size_t begin, std::size_t end) { return std::accumulate(v.begin() + begin, v.begin() + end, 0LL); } int main() { std::vector<int> data(1'000'000); std::iota(data.begin(), data.end(), 1); auto first_half = std::async(std::launch::async, sum_range, std::cref(data), 0, data.size() / 2); long long second_half = sum_range(data, data.size() / 2, data.size()); std::cout << "total = " << first_half.get() + second_half << '\n'; }
total = 500000500000
Parallel algorithms
Many standard algorithms accept an execution policy as a first argument — std::execution::par asks the library to run them in parallel, e.g. std::sort(std::execution::par, v.begin(), v.end()). Support varies: GCC needs Intel's TBB library for it, and Apple's libc++ doesn't provide it yet.
Advice
- Share as little mutable data between threads as possible; give each thread its own data and combine results at the end.
- Protect anything shared with a mutex or make it atomic.
- Use RAII lock guards, never manual
lock()/unlock(), and join every thread (or usestd::jthread). - Prefer higher-level tools (
std::async, parallel algorithms, thread pools from libraries) to managing threads by hand. - Test with ThreadSanitizer.
Try it yourself
Count the primes below 200,000 using four threads, each checking a quarter of the range and adding to an atomic counter.
17984 primes below 200000