Multicore hardware is the norm, and data races are undefined behaviour, so correct synchronisation is a core skill.
std::thread starts a function on a new thread. You must join() it (wait) or detach() it before the std::thread object is destroyed, or the program calls std::terminate. C++20's `std::jthread` joins automatically in its destructor (RAII again) and supports cooperative cancellation: the thread function can take a std::stop_token and poll stop_requested(), and request_stop() asks it to finish. Prefer jthread.
When threads share data and at least one writes, you need synchronization. An unsynchronized concurrent read and write is a data race, which is undefined behaviour, not just a wrong number.
std::mutexprovides mutual exclusion. Never calllock()/unlock()by hand; usestd::scoped_lock lock{m};(C++17), which unlocks at scope exit even on exceptions.scoped_lockcan lock several mutexes at once without deadlock. Usestd::unique_lockwhen you need to unlock early or wait on a condition variable.std::condition_variablelets a thread sleep until another notifies it that a condition (checked under the mutex) has become true.std::atomic<T>makes individual operations on small values (++,load,store,fetch_add,compare_exchange) indivisible without a lock. Good for counters and flags; for invariants spanning several variables, use a mutex.
Higher-level tools: std::async returns a std::future with the result; C++20 adds std::latch, std::barrier and std::counting_semaphore.
Practical advice: share as little as possible, hold locks briefly and never call unknown code while holding one, and pass data to threads by value. Capturing locals by reference in a thread lambda is only safe if the thread is joined before those locals die. Test with -fsanitize=thread, which detects data races at run time.
Example
#include <atomic>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
std::atomic<int> hits{0}; // indivisible ++, no data race
std::mutex m;
std::vector<int> finished; // shared, guarded by m
{
std::vector<std::jthread> workers; // C++20: joins in its destructor
for (int id = 0; id < 4; ++id) {
workers.emplace_back([&, id] {
for (int i = 0; i < 1000; ++i) ++hits;
std::scoped_lock lock{m}; // unlocks at end of scope
finished.push_back(id);
});
}
} // all threads joined here
std::cout << hits << ' ' << finished.size() << '\n'; // 4000 4
std::jthread ticker([](std::stop_token st) { // cooperative cancellation
while (!st.stop_requested()) std::this_thread::yield();
});
ticker.request_stop(); // the destructor would do this too, then join
}Remember
- A data race (unsynchronised write + access) is undefined behaviour
- Prefer
std::jthread; lock withstd::scoped_lock, never manual lock/unlock - Atomics for single counters/flags, mutexes for multi-variable invariants
Watch out for
- Letting a
std::threadobject be destroyed while still joinable, which callsstd::terminate.