By the end of this chapter you can
- Start and join a thread correctly
- Explain what happens if a joinable thread is destroyed
- Use jthread and stop tokens
A thread is a second place your program can be executing at once. On a machine with more than one core that means real parallelism; on one core it means the operating system slicing time between them. Either way, the moment you start one you have given up the single most useful property your programs have had so far: that statements happen in the order they are written.
This part of the book is about what you get in exchange and what it costs. This chapter is only about starting a thread and getting it back — which sounds like the easy part, and contains three ways to end your process.
#include <cstdio>
#include <thread>
#include <vector>
int main() {
std::printf("hardware_concurrency reports %u\n", std::thread::hardware_concurrency());
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i)
workers.emplace_back([i] { std::printf("worker %d running\n", i); });
for (std::thread& worker : workers) worker.join();
std::printf("all four finished\n");
}std::thread’s constructor takes anything callable and starts running it
immediately. join() blocks until that function returns. Between those two
lines, two things are happening at once and you cannot say which order the four
printf lines will come out in — run it a few times.
hardware_concurrency() is a hint: the number of threads that could genuinely
run at once. It is allowed to return 0 if the implementation cannot tell.
A std::thread is a handle you must dispose of
std::thread owns an operating-system thread the way unique_ptr owns memory,
with one loud difference: its destructor does not clean up. Destroying a
std::thread that is still joinable — started and neither joined nor
detached — calls std::terminate and kills the process.
#include <cstdio>
#include <thread>
int main() {
{
std::thread worker([] { std::printf("worker running\n"); });
} // worker is destroyed here, still joinable
std::printf("this line is never reached\n");
}terminate called without an active exception
detach, and why it is rarely what you want
The other way to dispose of a thread is to disown it:
#include <chrono>
#include <cstdio>
#include <thread>
int main() {
std::thread([] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::printf("detached thread finished\n"); // never printed
}).detach();
std::printf("main is done\n");
}Only one line appears. main returns, the process exits, and the detached
thread is killed mid-sleep without ceremony. Nothing warns you.
Detaching also removes your last way of knowing when the thread is done, which means you can no longer reason about the lifetime of anything it refers to. A detached thread capturing a local by reference is a use-after-free the moment its creator returns.
There are real uses — a genuinely fire-and-forget background task in a program
that runs forever — and they are rarer than the number of detach() calls in
the wild suggests.
std::jthread: the one you should use
C++20 added std::jthread, which does the obvious thing: joins in its
destructor.
#include <cstdio>
#include <thread>
#include <vector>
int main() {
{
std::vector<std::jthread> workers;
for (int i = 0; i < 3; ++i)
workers.emplace_back([i] { std::printf("worker %d running\n", i); });
} // every jthread joins here, in order
std::printf("all joined, no terminate\n");
}That is RAII applied to threads, and it is the same argument Chapter 3.2 made about files and memory: the destructor is the only place cleanup happens reliably, because it also happens when an exception unwinds past it.
Prefer std::jthread to std::thread by default. You still call join()
explicitly when you need the result before the end of the scope; the destructor
is there for every path where you do not.
Stop tokens: asking a thread to finish
A joining destructor is only useful if the thread will actually finish. A worker
looping until told otherwise will block the destructor forever — so jthread
carries the other half: a cooperative cancellation channel.
#include <chrono>
#include <cstdio>
#include <stop_token>
#include <thread>
int main() {
std::jthread worker([](std::stop_token stop) {
int ticks = 0;
while (!stop.stop_requested()) {
++ticks;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
std::printf("worker stopped after %d ticks\n", ticks);
});
std::this_thread::sleep_for(std::chrono::milliseconds(60));
std::printf("main is requesting a stop\n");
worker.request_stop();
} // the destructor requests a stop too, then joinsIf the callable’s first parameter is a std::stop_token, jthread passes one
in. request_stop() sets the flag; the worker sees it on its next check and
returns. The destructor calls request_stop() before joining, so even the code
above would terminate correctly with the explicit call removed.
The word to hold on to is cooperative. Nothing interrupts the thread. If the
worker is blocked in a long sleep_for, or in a read from a socket, it will not
notice the request until it comes back and checks. There is no portable way to
kill a thread from outside, and every platform-specific one leaves locks held
and destructors unrun.
Passing data in
std::thread and std::jthread copy their arguments into storage the new
thread owns, then invoke the callable with those copies as rvalues. That is
deliberate: the caller’s stack frame may be gone by the time the thread runs.
The consequence is that passing a reference does not work by accident:
#include <thread>
void bump(int& n) { ++n; }
int main() {
int value = 0;
std::thread t(bump, value); // value is copied; the copy is an rvalue
t.join();
return value;
}static assertion failed: std::thread arguments must be invocable after conversion to rvalues
The fix is to say you meant it:
#include <cstdio>
#include <functional>
#include <thread>
void bump(int& n) { ++n; }
int main() {
int value = 0;
std::thread t(bump, std::ref(value));
t.join();
std::printf("value is now %d\n", value);
}Two threads, one std::cout
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<std::jthread> workers;
for (int i = 0; i < 4; ++i)
workers.emplace_back([i] {
for (int line = 0; line < 3; ++line)
std::cout << "thread " << i << " line " << line << '\n';
});
}The output is interleaved mid-line — thread thread 0 line 10 line 0. This is
not a data race and not undefined behaviour: the standard stream objects are
synchronised, so concurrent writes do not corrupt anything. What is not
guaranteed is that a chain of << operators stays together, because it is four
separate function calls with nothing holding a lock across them.
Two fixes. Build the whole line first and write it in one call:
std::cout << ("thread " + std::to_string(i) + " line " + std::to_string(line) + "\n");or use std::printf, which is required to be atomic per call — which is why
every other sample in this chapter uses it.
For anything more than diagnostics, log through one object that owns a mutex. Chapter 8.2 is about how to build that correctly.
How many threads?
Not one per task. Threads are not free: each gets a stack, typically 8 MB of reserved address space, and switching between them costs the operating system real work. A thousand threads on four cores spend most of their time being scheduled.
The usual shape is a pool: hardware_concurrency() threads, fed a queue of
work items. Chapter 8.4 builds up to that with std::async and futures, and
explains why the standard library still has no thread pool of its own.