std::thread — Creating and Joining Threads
std::thread(C++11) is the standard library's mechanism for running code concurrently in a separate OS thread. A std::thread object represents exactly one thread of execution: it starts running when the object is created and you must explicitly join it (wait for it to finish) or detach it (let it run independently) before the object is destroyed, or the program terminates. This explicitness is intentional — the C++ runtime refuses to silently discard work you started.
Creating a thread
Pass any callable — a function pointer, a lambda, a function object — to the std::thread constructor. The thread starts running immediately. Arguments following the callable are forwarded to it, by value unless you wrap them with std::ref.
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Thread " << id << " running\n";
}
int main() {
std::thread t1(worker, 1); // starts immediately, calls worker(1)
std::thread t2(worker, 2); // second thread, calls worker(2)
t1.join(); // wait for t1 to finish
t2.join(); // wait for t2 to finish
// both threads have completed here
}
// Lambda syntax (most common):
std::thread t3([] {
std::cout << "lambda thread\n";
});
t3.join();
// Capturing by reference — dangerous if the thread outlives the local:
int result = 0;
std::thread t4([&result] {
result = compute(); // modifies caller's variable
});
t4.join(); // must join before result goes out of scope!
std::cout << result;join() vs detach()
Every std::threadthat represents a running thread must be either joined or detached before its destructor runs. If a joinable thread's destructor runs (e.g., because an exception unwinds the stack), std::terminate() is called — your program crashes.
// join — wait for the thread to complete:
std::thread t(do_work);
t.join(); // blocks until do_work() returns
// t is no longer joinable after join()
// detach — release the thread to run on its own:
std::thread t2(background_task);
t2.detach(); // t2 no longer manages the thread
// the thread continues running independently until do_work() returns
// Check before joining:
if (t.joinable()) t.join(); // safe even if already joined or default-constructed
// WRONG — destructor called on joinable thread → std::terminate:
{
std::thread t3(do_work);
// forgot to join or detach!
} // CRASH — destructor calls std::terminate()Detached threads are hard to reason about and create lifetime hazards if they access local variables. Prefer join()or use C++20's std::jthread which joins automatically.
Passing arguments to threads
Arguments are passed to the thread function by value by default — they are copied into the thread's internal storage before the thread starts. To pass by reference, wrap with std::ref() (or std::cref() for const reference). This is necessary because std::thread stores all arguments internally, and a bare reference would be unsafely stored.
void accumulate(const std::vector<int>& data, long long& result) {
result = 0;
for (int v : data) result += v;
}
std::vector<int> numbers = {1, 2, 3, 4, 5};
long long total = 0;
// WRONG — 'total' would be passed by value (copied), not by reference:
// std::thread t(accumulate, numbers, total);
// CORRECT — std::ref signals intent to pass by reference:
std::thread t(accumulate, std::cref(numbers), std::ref(total));
t.join();
std::cout << total; // 15
// Move-only types must be moved in:
std::unique_ptr<Widget> w = std::make_unique<Widget>();
std::thread t2(process, std::move(w)); // w is moved into the thread
t2.join();Thread count and IDs
// Query the number of hardware threads (logical cores):
unsigned int n = std::thread::hardware_concurrency();
// Returns 0 if the value cannot be determined
// Get the current thread's ID:
std::thread::id tid = std::this_thread::get_id();
std::cout << "Running in thread: " << tid << "\n";
// Get a thread object's ID:
std::thread t(do_work);
std::thread::id worker_id = t.get_id();
t.join();
// Useful for creating a thread pool:
const unsigned nthreads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> pool;
pool.reserve(nthreads);
for (unsigned i = 0; i < nthreads; ++i)
pool.emplace_back(worker_task, i);
for (auto& t : pool) t.join();std::jthread (C++20) — automatic join
std::jthread (C++20) is like std::thread but automatically joins in its destructor — the same RAII pattern as smart pointers for memory. It also supports cooperative cancellation through std::stop_token, letting you signal a thread that it should stop working.
#include <thread> // also contains jthread
// jthread automatically joins on destruction — no crash, no explicit join:
{
std::jthread t(do_work);
// if an exception throws here, t's destructor joins cleanly
} // t.join() called automatically — always safe
// Cooperative cancellation with stop_token:
std::jthread t([](std::stop_token stop) {
while (!stop.stop_requested()) {
process_one_item();
}
});
// Signal the thread to stop:
t.request_stop(); // sets the stop_token
t.join(); // wait for it to notice and exitKey rules to remember
Every joinable thread must be joined or detached before its std::thread destructor runs
Failing to do so calls std::terminate(). Use RAII wrappers or std::jthread (C++20) to make this automatic.
Arguments are passed by value — use std::ref() to pass by reference
std::thread copies arguments into internal storage. Accidentally passing a reference without std::ref results in a copy, not a reference to the original.
Capturing references in lambdas is safe only if the thread joins before the referenced variable is destroyed
If the thread outlives the local variable it captures by reference, that reference becomes dangling — undefined behavior.
Prefer std::jthread over std::thread when targeting C++20
It auto-joins, supports stop tokens for cancellation, and eliminates the 'forget to join → terminate' footgun.