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.

Synchronization Primitives at a Glance

mutex/lock_guard for mutual exclusion

See Also

  • C: Threads — C11 <threads.h>, without RAII lock guards or `std::jthread’s cooperative cancellation.