Cooperative Interruption of a Thread in C++20
A typical question in my C++ seminars is: Can a thread be killed? Before C++20, my answer is no. With C++20, you can ask a thread politely for its interruption.
First of all. Why is it not a good idea to kill a thread? The answer is relatively easy. You don’t know which state the thread is in when you kill it. Here are two possible malicious outcomes.
- The thread is only half-done with its job. Consequently, you don’t know the state of that job and, hence, the state of your program. You end with undefined behavior, and all bets are open.
- The thread may be in a critical section and locks a mutex. Killing a thread while it locks a mutex ends with a high probability of deadlock.
Okay, killing a thread is not a good idea. You may ask a thread friendly if it is willing to stop. This is precisely what cooperative interruption in C++20 means. You ask the thread, and the thread can accept or ignore your wish for the interruption.
Cooperative Interruption
The additional functionality of the cooperative interruption thread in C++20 is based on the std::stop_token
, the std::stop_callback
, and the std::stop_source
data types.
std::stop_token
, std::stop_callback
, and std::stop_source
A std::stop_token
, a std::stop_callback
, or a std::stop_source
allows a thread to asynchronously request an execution to stop or ask if an execution got a stop signal. The std::stop_token
can be passed to an operation and afterward be used to poll the token for a stop request actively or to register a callback via std::stop_callback
. The stop request is sent by a std::stop_source
. This signal affects all associated std::stop_token
. The three classes std::stop_source
, std::stop_token
, and std::stop_callback
share the ownership of an associated stop state. The calls request_stop()
, stop_requested()
, and stop_possible()
are atomic.
You can construct a std::stop_source
in two ways:
stop_source(); // (1) explicit stop_source(std::nostopstate_t) noexcept; // (2)
The default constructor (1) constructs a std::stop_source
with a new stop state. The constructor taking std::nostopstate_t
(2) constructs an empty std::stop_source
without associated stop state.
The component std::stop_source src
provides the following member functions for handling stop requests.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
src.stop_possible()
means that src
has an associated stop state. src.stop_requested()
returns true
when src
has an associated stop state and was not asked to stop earlier. src.request_stop()
is successful and returns true
if src
has an associated stop state, which was not requested to stop before.
The call src.get_token()
returns the stop token stoken
. Thanks to stoken
you can check if a stop requ
est has been made or can be made for its associated stop source src
. The stop token stoken
observes the stop source src
.
The following table presents the member functions of a std::stop_token stoken
.
A default-constructed token that has no associated stop state. stoken.stop_possible
also returns true
if stoken
has an associated stop state. stoken.stop_requested()
returns true
when the stop token has an associated stop state and has already received a stop request.
If the std::stop_token
should be temporarily disabled, you can replace it with a default constructed token. A default constructed token has no associated stop-state. The following code snippet shows how to disable and enable a thread’s capability to accept stop requests.
std::jthread jthr([](std::stop_token stoken) { ... std::stop_token interruptDisabled; std::swap(stoken, interruptDisabled); // (1) ... // (2) std::swap(stoken, interruptDisabled); ... }
std::stop_token interruptDisabled
has no associated stop state. This means the thread jthr
can, in all lines except lines (1) and (2), accept stop requests.
When you study the code snippet carefully, you may wonder if the used std::jthread. std::jthread
in C++20 is an extended std::thread
in C++11. The j in jthread
stands for joinable because it joins automatically in its destructor. Its first name was ithread
. You may guess why: i stands for interruptible. I will present std::jthread
in my next post.
My next example shows the use of callbacks using a std::jthread.
// invokeCallback.cpp #include <chrono> #include <iostream> #include <thread> #include <vector> using namespace::std::literals; auto func = [](std::stop_token stoken) { // (1) int counter{0}; auto thread_id = std::this_thread::get_id(); std::stop_callback callBack(stoken, [&counter, thread_id] { // (2) std::cout << "Thread id: " << thread_id << "; counter: " << counter << '\n'; }); while (counter < 10) { std::this_thread::sleep_for(0.2s); ++counter; } }; int main() { std::cout << '\n'; std::vector<std::jthread> vecThreads(10); for(auto& thr: vecThreads) thr = std::jthread(func); std::this_thread::sleep_for(1s); // (3) for(auto& thr: vecThreads) thr.request_stop(); // (4) std::cout << '\n'; }
Each of the ten threads invokes the lambda function func
(1). The callback (2) displays the thread id
and the counter
. Due to the one-second sleeping of the main thread (3) and the sleeping of the child threads, the counter is 4 when the callbacks are invoked. The call thr.request_stop()
triggers the callback on each thread.
What’s next?
As mentioned, std::thread
has one significant weakness. When you forget to join it, its destructor calls std::terminate
, and your program crashes. std::jthread
(C++20) overcomes this counter-intuitive weakness and is also interruptible.
Repetition
You may have noticed it. This post is a repetition of a post I have written 3 1/2 years ago. The reason is simple. I need this post as a prerequisite for my next post, and I don’t want to write a post two times.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!