std::jthread stopping
Canonical way to stop a std::jthread, using std::stop_token and
std::stop_callback.
Canonical example
Assuming you want to run in a child thread some example_server similar to the
ones provided by Python socketserver:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void foo() {
std::jthread t([](std::stop_token token) {
// ...
auto callback = std::stop_callback(
token,
[&](){
// function usually called from the parent thread
// to make the child thread exit the processing loop
example_server.shutdown();
}
);
// child thread blocked in some processing loop
example_server.serve_forever();
});
}
Explanation
std::thread (without j), introduced in C++11, does not join in the
destructor, i.e. it does not wait for the underlying thread to stop. If the
destructor is reached and the underlying thread is still running,
std::terminate() will be called. You could detach the std::thread object
from the underlying thread, but that’s usually a bad idea: for example the
underlying thread uses objects that might go out of scope. So therefore you
need to ensure that the underlying thread stops before the std::thread object
is destroyed.
std::jthread, introduced in C++20, joins in the destructor. But what makes it
more useful than std::thread is that it also provides a mechanism to
communicate to the underlying thread to stop. That mechanism is based on
std::stop_source, std::stop_token and std::stop_callback.
std::jthread has a std::stop_source. For the canonical usage of
std::jthread you would use a functor that takes a std::stop_token as an
argument. std::jthread will provide the functor with a std::stop_token
linked to the std::stop_source that it owns. In the std::jthread
destructor, it will request_stop() on that std::stop_source before
joining. The underlying thread will thus know that it needs to stop and will
exit.
Another option: poll
You can poll, an option if the child thread does not block longer that you’re willing to wait in the parent thread:
1
2
3
4
5
6
7
8
void foo() {
std::jthread t([](std::stop_token token) {
// ...
while (!token.stop_requested()) {
// do stuff
}
});
}
Another option: condition variable
If you want to exit a blocking condition variable when a stop is requested, you
can do it with a std::condition_variable_any:
1
2
3
4
5
6
7
8
9
void foo() {
std::jthread t([] (std::stop_token stoken) {
std::mutex mutex;
std::unique_lock lock(mutex);
std::condition_variable_any cv;
cv.wait(lock, stoken,
[&stoken] { return stoken.stop_requested(); });
});
}
FAQ
Q: How does std::stop_callback handles the race case where the
request_stop() was already called on the std::stop_source in the parent
thread?
A: In that case the functor provided to std::stop_callback is called when
the std::stop_callback is constructed in the child thread.
Q: What about the case where the std::stop_callback functor is being
executed from the parent thread, while the std::stop_callback is destructed
in the child thread?
A: The destructor of the std::stop_callback will wait for the functor to
complete before continuing the destruction.
Q: How does std::stop_callback handles the race case where it is
destructed before request_stop()is called on the std::stop_source in the
parent thread?
A: In that case the functor provided to std::stop_callback is not called.
Q: What’s std::inplace_stop_source()?
A: It was introduced in C++26 to support low level concurrency primitives
around coroutines, sender/receiver without allocating when a stop source is
constructed. std::stop_source() heap allocates a shared state which means:
constructing can throw, copy is noexcept and stop_possible() exists to
return false if it does not have a shared state (e.g. moved from object).
How do they work?
The stop source has a bool that can be toggled on with request_stop(). It
also has a list of current stop callbacks. When request_stop() toggles the
bool, it will also traverse the list and call the currently registered stop
callbacks.
The stop token is “just” a pointer to the stop_source. It is passed by value.
The holder of a stop token cannot request_stop(), but can check
stop_requested().
The stop callback uses the stop token to reach the list of current stop callbacks and insert a node that it provides.
Implementing such types to handle multithreaded synchronization is a medium to hard exercise.