std::jthread and stop tokens
A std::thread that is still joinable when it is destroyed terminates the program, as lesson 105 showed. std::jthread from C++20 joins in its destructor, so the same code is safe without a line of cleanup.
It carries a second gift: a std::stop_token that the thread can poll, and a request_stop that sets it. That is cooperative cancellation, the only kind C++ offers, and it replaces the hand-made atomic flag that a worker loop used to need.
A stop token can be copied and passed to another thread, and every copy refers to the same stop state, so one request_stop reaches a whole group of workers. That is how a parent stops the tasks it started without knowing which of them is currently awake.
The token is shared state, not a message: a request that arrives before the worker starts is still visible to it, which is the property a hand-written atomic flag has to be carefully built to keep.
The destructor joins
A jthread takes the same callable as a thread, and its destructor calls request_stop and then joins. The thread finishes or notices the stop request before the destructor returns.
The program sets 42 in a worker and reads it after the block. Without the braces the read could race with the write; with them the destructor has already joined, so the read is ordered after the assignment.
The name is the whole difference from std::thread: the j is for joining. A jthread also has a join() for the code that wants to wait earlier, and joinable() and get_id() behave as before.
#include <iostream>
#include <thread>
int main()
{
int value = 0;
{
std::jthread worker{[&value] { value = 42; }};
}
std::cout << value << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread join.cpp -o join
./join
42
A loop that stops
A thread whose first parameter is a std::stop_token receives the token of its jthread. stop_requested() answers whether someone asked it to stop, and the loop decides how often to look.
The worker polls the token every millisecond, and main waits 20 milliseconds and calls request_stop. The destructor then joins, and the message from the worker is printed before the program ends.
The token does not interrupt anything: the loop has to check it. A thread blocked in a read or a sleep for a long time will not notice a request until it returns, which is why the poll interval is part of the design.
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
std::jthread worker{[](std::stop_token token) {
while (!token.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::cout << "worker stopped\n";
}};
std::this_thread::sleep_for(std::chrono::milliseconds(20));
worker.request_stop();
}
g++ -std=c++23 -Wall -Wextra -pthread token.cpp -o token
./token
worker stopped
A callback on stop
std::stop_callback registers a function that runs when the stop is requested, either on the thread that calls request_stop or immediately when the token is already stopped. It is how a blocking operation is woken instead of polled.
The program registers a callback that increments a counter and prints the counter after the loop. It is 1, whether the request arrived before or after the callback was registered.
A callback must be quick and must not throw. Its job is to set another flag or to close a handle that the blocked call is waiting on, not to do the work of the thread.
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
int main()
{
std::jthread worker{[](std::stop_token token) {
int calls = 0;
std::stop_callback callback{token, [&calls] { ++calls; }};
while (!token.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::cout << calls << '\n';
}};
worker.request_stop();
}
g++ -std=c++23 -Wall -Wextra -pthread callback.cpp -o callback
./callback
1
A thread has no stop
The stop machinery belongs to jthread, not to thread. Calling request_stop on a std::thread is a compile error, and the message says the class has no member of that name.
The program starts a plain thread and calls request_stop on the object. The compiler lists the members that do exist, and none of them can ask the function to finish.
The fix is to change the type to std::jthread and give the body a stop_token parameter, or to pass an atomic flag that the loop checks. The second is what the standard library itself did before C++20, and it is the version to recognise in older code.
#include <thread>
int main()
{
std::thread worker{[] {}};
worker.request_stop();
worker.join();
}
g++ -std=c++23 -Wall -Wextra -pthread plain.cpp -o plain
plain.cpp: In function 'int main()':
plain.cpp:7:12: error: 'class std::thread' has no member named 'request_stop'
7 | worker.request_stop();
| ^~~~~~~~~~~~
Exercises
- Make the worker count its iterations and print the count when it stops; run it twice and explain the difference.
- Use a stop_callback to set a second flag that the worker checks in its loop.
- Replace the jthread in the first program with a thread and a join and check that the output is the same.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a
std::jthreaddo that astd::threaddoes not?Answer
It joins its thread in the destructor and carries a stop_token
The destructor requests a stop and joins, which is where the j in the name comes from.
-
What does
request_stop()do?Answer
It sets the stop state and runs the registered stop callbacks
The request is cooperative: the token becomes stopped and the callbacks run, and the thread ends when it notices.
-
Why must the worker check
stop_requested()itself?Answer
Because cancellation is cooperative and nothing interrupts the loop
C++ has no way to stop a thread from outside; the thread has to look at the token.
-
What does this program print?
#include <iostream> #include <thread> int main() { int value = 0; { std::jthread worker([&value] { value = 42; }); } std::cout << value; }Answer
42
The destructor of the jthread joins before the print, so the write is complete.
-
What does this program print?
#include <chrono> #include <iostream> #include <thread> int main() { std::jthread worker([](std::stop_token token) { while (!token.stop_requested()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::cout << "worker stopped"; }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); worker.request_stop(); }Answer
worker stopped
The loop ends on the request and the worker prints once; the destructor joins it.
-
What does this program print?
#include <chrono> #include <iostream> #include <stop_token> #include <thread> int main() { std::jthread worker([](std::stop_token token) { int calls = 0; std::stop_callback callback{token, [&calls] { ++calls; }}; while (!token.stop_requested()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::cout << calls; }); worker.request_stop(); }Answer
1
The callback runs exactly once, when the stop is requested.
-
When does a
std::stop_callbackrun?Answer
When the stop is requested, or immediately if it is already requested
Registration on an already-stopped token runs the callback at once, so no request is missed.
-
Does
request_stop()end the thread?Answer
No, it asks, and the thread ends when it notices
The standard has no forcible termination; the function has to return.
-
How does a jthread pass the stop token to the function?
Answer
As a first parameter of type
std::stop_tokenThe jthread constructs the function with its token first, and the signature declares it.
-
What does the compiler answer to
worker.request_stop()on astd::thread?Answer
That std::thread has no member named request_stop
The member belongs to jthread, so the call does not resolve on a thread object.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev