Skip to content

std::thread, std::jthread & Thread Management

Learn to create and manage threads with std::thread and C++20's std::jthread. Understand join vs detach, cooperative cancellation with stop tokens, thread-local storage, and exception handling across thread boundaries.

Why Concurrency?

Modern CPUs have multiple cores, and to exploit them your program must run work concurrently. C++11 introduced a portable threading library in , and C++20 improved it with std::jthread. A thread is an independent path of execution that shares the same address space as the thread that created it — which is both its power and its danger.

Unlike processes, threads share memory. That makes communication fast but also introduces data races if two threads read and write the same variable without synchronization. Before we tackle synchronization (next lesson), let us first learn how to create, manage, and cleanly shut down threads.

Creating Threads with std::thread

You create a std::thread by passing it a callable (function, lambda, functor) and any arguments. The thread begins executing immediately upon construction.

create_thread.cpp
#include <iostream>
#include <thread>

void greet(const std::string& name, int times) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
    }
}

int main() {
    // Launch a thread running greet("Alice", 3)
    std::thread t1(greet, "Alice", 3);

    // Lambda-based thread
    std::thread t2([]() {
        std::cout << "Running in a lambda thread\n";
    });

    // MUST join or detach before t1/t2 are destroyed
    t1.join();  // block until t1 finishes
    t2.join();

    std::cout << "All threads finished\n";
    return 0;
}

join() vs detach()

Every std::thread that represents a running thread must be either joined or detached before its destructor runs. If you forget, std::terminate() is called and your program crashes.

join() blocks the calling thread until the target thread finishes. This is the safe default — you know the thread is done and its results are ready.

detach() severs the connection between the std::thread object and the OS thread. The thread runs in the background (a "daemon thread") and you can no longer join it. Be cautious: if the detached thread accesses stack variables from the spawning function and that function returns, you get undefined behavior.

Pitfall

Never detach() a thread that captures or references local variables from the creating scope. When the creating function returns, those locals are destroyed and the detached thread accesses dangling memory. This is undefined behavior and often manifests as sporadic crashes. If you must detach, pass data by value or use std::shared_ptr to extend lifetimes.

std::jthread — The RAII Thread (C++20)

std::jthread solves two problems with std::thread: (1) it automatically joins in its destructor, so you can never forget, and (2) it supports cooperative cancellation via std::stop_token.

jthread_demo.cpp
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken, int id) {
    while (!stoken.stop_requested()) {
        std::cout << "Worker " << id << " running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "Worker " << id << " stopping gracefully\n";
}

int main() {
    std::jthread jt1(worker, 1);  // stop_token is auto-passed as first arg
    std::jthread jt2(worker, 2);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Request both threads to stop
    jt1.request_stop();
    jt2.request_stop();

    // No need to call join() — jthread's destructor does it
    std::cout << "Main thread exiting\n";
    return 0;
}

Querying Hardware & Thread-Local Storage

std::thread::hardware_concurrency() returns the number of concurrent threads the hardware supports. The thread_local storage duration gives each thread its own copy of a variable.

hardware_tls.cpp
#include <iostream>
#include <thread>
#include <vector>

thread_local int tls_counter = 0;  // each thread gets its own copy

void count_work(int id) {
    for (int i = 0; i < 1000; ++i) {
        ++tls_counter;  // no data race — each thread has its own
    }
    std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}

int main() {
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "Hardware concurrency: " << n << " threads\n";

    std::vector<std::thread> threads;
    for (unsigned int i = 0; i < n; ++i) {
        threads.emplace_back(count_work, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    // Each thread printed 1000 — they don't share tls_counter
    return 0;
}
Best Practice

In C++20 and later, prefer std::jthread over std::thread. It automatically joins in its destructor, eliminating the risk of calling std::terminate if you forget to join. Its built-in std::stop_token mechanism provides a clean, standardized way to request cooperative cancellation — no need to roll your own atomic shouldStop flag.

Exception Handling Across Threads

An exception thrown inside a thread that is not caught within that thread calls std::terminate(). Exceptions do not propagate automatically to the joining thread.

To transfer exceptions between threads, capture them with std::current_exception() inside the worker thread and store them in a std::exception_ptr. The joining thread can then rethrow with std::rethrow_exception(). The std::future mechanism (covered in Lesson 4) handles this automatically.

Key Takeaways
  • std::thread starts executing immediately; you must join() or detach() before its destructor runs
  • std::jthread (C++20) auto-joins in its destructor and supports cooperative cancellation via std::stop_token
  • thread_local gives each thread its own independent copy of a variable — no synchronization needed
  • std::thread::hardware_concurrency() tells you how many threads the hardware can run concurrently
  • Exceptions in a thread do not propagate to the caller — use std::exception_ptr or std::future to transfer them
  • Never let a detached thread reference local variables from the spawning scope

Quiz — Test Your Knowledge

(15 XP)

1. What happens if a `std::thread` object representing a joinable thread is destroyed without calling `join()` or `detach()`?

2. How does `std::jthread` support cooperative cancellation?

3. What does the `thread_local` keyword do?