Threads and Synchronization
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
std::thread and std::jthread
#include <thread>
#include <iostream>
void work(int id) {
std::cout << "worker " << id << '\n';
}
int main() {
std::thread t(work, 1);
t.join(); // must join() or detach() before a std::thread is destroyed --
// otherwise std::terminate is called
std::jthread jt(work, 2); // C++20: automatically joins in its destructor -- no manual join() needed
}
Prefer std::jthread in new code — forgetting to join()/detach() a plain std::thread is a classic bug
that `jthread’s RAII destructor eliminates entirely.
stop_token Cancellation
std::jthread also wires up cooperative cancellation automatically — the running function can accept a
std::stop_token and periodically check it:
#include <thread>
#include <chrono>
#include <iostream>
void pollingWork(std::stop_token token) {
while (!token.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
std::cout << "stopped cooperatively\n";
}
int main() {
std::jthread jt(pollingWork);
std::this_thread::sleep_for(std::chrono::milliseconds(120));
jt.request_stop(); // also happens automatically when jt is destroyed
}
Mutexes and Lock Helpers
#include <mutex>
#include <shared_mutex>
std::mutex m;
int sharedCounter = 0;
void increment() {
std::lock_guard<std::mutex> lock(m); // locks on construction, unlocks on destruction -- exception-safe
++sharedCounter;
}
std::mutex m1, m2;
void transferSafely() {
std::scoped_lock lock(m1, m2); // C++17: locks BOTH atomically, avoiding a classic deadlock from
} // two threads locking m1/m2 in opposite order
void conditionalWork() {
std::unique_lock<std::mutex> lock(m); // more flexible than lock_guard: can unlock/relock, or defer
lock.unlock(); // locking -- required by condition_variable::wait, below
// ... unlocked work ...
lock.lock();
}
std::shared_mutex rw;
int cachedValue = 0;
int readValue() {
std::shared_lock<std::shared_mutex> lock(rw); // multiple readers may hold this simultaneously
return cachedValue;
}
void writeValue(int v) {
std::unique_lock<std::shared_mutex> lock(rw); // exclusive: blocks all readers and other writers
cachedValue = v;
}
Condition Variables
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex m;
std::condition_variable cv;
std::queue<int> queue;
void producer() {
{
std::lock_guard<std::mutex> lock(m);
queue.push(42);
}
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return !queue.empty(); }); // atomically unlocks while waiting, relocks before
int value = queue.front(); // returning -- the predicate guards against spurious wakes
queue.pop();
(void)value;
}
latch, barrier, and counting_semaphore
Three C++20 coordination primitives, each for a distinct pattern:
#include <latch>
#include <barrier>
#include <semaphore>
#include <thread>
#include <vector>
void latchDemo() {
std::latch done(3); // a one-shot countdown: wait() blocks until count reaches 0
std::vector<std::jthread> workers;
for (int i = 0; i < 3; ++i) {
workers.emplace_back([&done] { done.count_down(); });
}
done.wait(); // main proceeds only once all 3 have counted down
}
void barrierDemo() {
std::barrier sync(3); // reusable: unlike latch, resets automatically for the next round
std::vector<std::jthread> workers;
for (int i = 0; i < 3; ++i) {
workers.emplace_back([&sync] { sync.arrive_and_wait(); }); // blocks until all 3 arrive, THEN releases
}
}
std::counting_semaphore<4> pool(4); // caps concurrent access to a limited resource (e.g. 4 connections)
void useResource() {
pool.acquire();
// ... use the limited resource ...
pool.release();
}
thread_local
A thread_local variable has its own independent instance per thread — initialized lazily on first use in
each thread:
#include <thread>
#include <iostream>
thread_local int counter = 0;
void increment() {
++counter; // each thread sees and modifies its OWN counter, no synchronization needed
std::cout << counter << '\n';
}
Exceptions From Threads
An exception that escapes a std::thread’s function calls `std::terminate — it does not propagate to the
joining thread. Catch it inside the thread function, or use std::async/std::promise (see
Async, Futures, and Atomics), where an exception
is captured and re-thrown from .get():
#include <thread>
#include <iostream>
#include <exception>
void safeWork() {
try {
throw std::runtime_error("boom");
} catch (const std::exception& e) {
std::cout << "handled inside the thread: " << e.what() << '\n';
}
}
std::osyncstream
Covered in Input, Output, and Streams — essential
whenever multiple threads write to std::cout concurrently, to avoid interleaved output.
See Also
-
C: Threads — C11
<threads.h>, without RAII lock guards or `std::jthread’s cooperative cancellation.