71 / 163 · C++20 · 10 min
Thread lifetime: thread, jthread, and stop_token
A thread object manages an execution resource; it is not the same thing as the thread still running. Decide who is responsible for waiting before you decide how long shared objects may live. C++20 jthread requests stop and waits automatically, but stop still requires cooperation from the work function and cannot forcibly abort a blocking operation.
In this lesson
Separate the executing thread from the managing object
Constructing std::thread lets the work function be scheduled; you do not wait for join() first. The thread object is only the handle that records who must later wait or detach. It is movable and not copyable. A move transfers that management duty and leaves the source in a state that is no longer joinable.
joinable() answers a narrower question than "is this still running". It is true while the object still represents a thread that has not been joined or detached, even if the work function has already returned. Only a successful join() ends that association. Destroying a still-joinable std::thread calls std::terminate, so every normal return and every exception path that owns the object must handle it.
detach() only gives up the ability to wait. It does not extend the lifetime of objects the work function captured by reference. A background thread that keeps using locals that have already left scope is a dangling access. Detaching the thread cannot repair a lifetime design that still needs those objects.
Use jthread to express scoped ownership
C++20 std::jthread requests stop and then waits if it is still joinable in its destructor. For a task with a clearly finite amount of work, automatic waiting is still useful even when the function does not take a stop token. The first part of the example sums a fixed hundred integers. The main thread reads the result only after leaving the inner scope, so the destructor has already joined.
Completion of the thread synchronizes with a successful return from the corresponding join(). The worker's writes to sum therefore happen-before the later read. The result does not need to become an atomic variable merely because it is accessed only around that handoff. Data referenced by the work function must be created before the thread object and destroyed after the thread finishes.
If an exception escapes the outermost work function, the program still terminates. Catch inside the worker and deliver the outcome through an agreed result channel. Scoped ownership solves waiting; it does not turn every callable into a cancellable, exception-safe task on its own.
Stop is a protocol, not a forced kill
If the callable accepts a first parameter of type std::stop_token, jthread passes its own stop token. request_stop() records a request. The work function responds by checking the token or by using a wait that supports stop. A successful request does not mean the thread has already finished. You still wait before releasing resources it uses.
The second part of the example uses the token overload of condition_variable_any. The predicate is always false, so only a stop request can make the wait return normally. The request can arrive before or during the wait. Neither sleep nor a particular scheduling order is required. Ordinary condition-variable waits and arbitrary blocking I/O do not automatically honor the token.
The standard does not promise that join() finishes within a fixed number of milliseconds. Keep the task finite and give every blocking point an exit path. A stop request is a cooperative protocol: it is visible to code that looks, and invisible to a blocking call that never checks.
Pitfalls
- Do not treat
joinable()as a query for "still computing"; a thread whose work function has returned but has not yet been joined is still joinable. - Do not destroy a
jthreadwhile holding a mutex the worker needs in order to exit: the destructor waits, and the worker may be waiting for that lock.
Run an example
Minimum C++20 · complete program · Download .cpp
#include <cassert>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <stop_token>
#include <thread>
int main() {
int sum = 0;
{
std::jthread worker([&] {
for (int i = 1; i <= 100; ++i) sum += i;
});
}
assert(sum == 5050);
std::mutex mutex;
std::condition_variable_any changed;
bool cancelled = false;
std::jthread waiter([&](std::stop_token token) {
std::unique_lock<std::mutex> lock(mutex);
const bool ready = changed.wait(lock, token, [] { return false; });
cancelled = !ready && token.stop_requested();
});
waiter.request_stop();
waiter.join();
assert(cancelled);
assert(!waiter.joinable());
std::cout << sum << ' ' << cancelled << '\n';
}
Compile locally
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread concurrency-threads.cpp -o example && ./exampleExpected result
5050 1
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If the second part is replaced with an unpredicated wait on an ordinary condition_variable, keeping only request_stop(), why is completion not guaranteed? How should it be changed?
Show a reference answer
An ordinary wait does not subscribe to stop state, so request_stop will not wake it automatically. Keep the example's condition_variable_any token overload. Another complete protocol is to protect done with the same mutex, have the main thread set done=true and notify_all, have the worker wait(lock, [&]{ return done; }), and then join. Extra notifies without a saved exit state can still lose a wakeup.
Check the sources
- cppreference: std::jthread destructor and stop support
- cppreference: condition_variable_any::wait stop_token overloads
- cppreference: thread::join synchronization guarantees
Drafts and official chapters change. The version mark is only the example’s minimum.