Softwareentwicklung: Kooperatives Unterbrechen eines Threads in C++20
Vor C++20 lieĂźen sich Threads nicht unterbrechen. Mit C++20 kann man an einen Thread die Anfrage stellen, dass er sich beendet. Ihr kann er dann nachkommen.

(Bild: Patrick Poendl/Shutterstock.com)
- Rainer Grimm
Dieser Artikel ist eine Wiederholung eines Beitrags, den ich ursprünglich vor dreieinhalb Jahre geschrieben habe. Ich benötige ihn als Einstieg für den nächsten Artikel. Daher habe ich mich entschieden, den Beitrag nochmals zu veröffentlichen.
Zuerst einmal: Warum ist es keine gute Idee, einen Thread zu beenden? Diese Antwort ist einfach. Man weiß nicht, in welchem Zustand der Thread ist, wenn man ihn beendet. Dies sind zwei mögliche Gefahren:
- Der Thread kann nur teilweise mit seinem Job fertig sein. Natürlich ist nun der Stand seines Jobs und damit auch der des Programms unbekannt. Am Ende führt das zum undefinierten Verhalten, und keine zuverlässige Aussage über das Programm ist mehr möglich.
- Der Thread kann sich gerade in einem kritischen Bereich befinden und einen Mutex gelockt haben. Wird der Thread in dieser Phase beendet, fĂĽhrt das mit hoher Wahrscheinlichkeit zu einem Deadlock.
Einen Thread abrupt zu unterbrechen, ist keine gute Idee. Da ist es schon besser, den Thread freundlich zu fragen, ob er sich beenden lassen will. Das ist genau, wofĂĽr kooperatives Unterbrechen in C++20 steht. Man fragt den Thread freundlich, ob er sich beenden will, und der Thread kann diesem Wunsch nachkommen oder ihn ignorieren.
Kooperatives Unterbrechen
Die zusätzliche Fähigkeit der kooperativen Unterbrechung in C++20 basiert auf den drei neuen Datentypen std::stop_token
, std::stop_callback
und std::stop_source
. Sie ermöglichen es einem Thread, einen Thread asynchron zu beenden oder zu fragen, ob ein Thread ein Stoppsignal erhalten hat. Der std::stop_toke
n
lässt sich dafür an eine Operation übergeben. Dieses Stopp-Token kann anschließend dazu verwendet werden, die Operation zu fragen, ob an sie der Wunsch zur Beendigung geschickt wurde. Anderseits lässt sich mit std::stop_token
ein Callback mittels std::stop_callback
registrieren. Die Stoppanfrage wird von std::stop_source
geschickt. Ihr Signal betrifft alle assoziierten std::stop_token
. Die drei Klassen std::stop_source
, std::stop_token
und std::stop_callback
teilen sich die Besitzverhältnisse des assoziierten Stoppzustands. Die Aufrufe request_stop()
, stop_requested()
und stop_possible()
sind atomar.
Ein std::stop_source
lässt sich auf zwei Arten erzeugen:
stop_source(); // (1)
explicit stop_source(std::nostopstate_t) noexcept; // (2)
Der Default-Konstruktor (1) erzeugt ein std::stop_source
mit einem Stoppzustand. Der Konstruktor, der std::nostopstate_t
als Argument annimmt, erzeugt eine std::stop_source
ohne assoziierten Stoppzustand.
Die Komponente std::stop_source src
bietet die folgenden Methoden an, um mit Stoppanfragen umzugehen:
src.stop_possible()
bedeutet, dass src
einen assoziierten Stoppzustand besitzt. src.stop_requested()
gibt dann true
zurĂĽck, wenn src
einen assoziierten Stoppzustand besitzt und nicht bereits frĂĽher zu stoppen angefordert wurde. Der Aufruf src.get_token()
gibt den Stopp-Token zurück. Dank ihm lässt sich prüfen, ob eine Stoppanfrage bereits erfolgt ist oder durchgeführt werden kann.
Das Stopp-Token stoken
beobachtet die Stoppquelle src
. Die folgende Tabelle stellt die Methoden der std::stop_token stoken
vor:
Ein Default-konstruiertes Token besitzt keinen assoziierten Stoppzustand. stoken.stop_possible
gibt true
zurĂĽck, falls stoken
einen assoziierten Stoppzustand besitzt. stoken_stop_requested()
gibt dann true
zurĂĽck, wenn der Stopp-Token einen assoziierten Stoppzustand besitzt und bereits eine Stoppanfrage erhalten hat.
Falls der std::stop_token
zeitweise deaktiviert werden soll, lässt er sich mit einem Default-konstruierten Token ersetzen. Dieses hat keinen assoziierten Stoppzustand. Die folgenden Zeilen zeigen, wie sich die Fähigkeit eines Threads, Stoppanfragen zu erhalten, zeitweise deaktivieren lässt:
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
besitzt keinen assoziierten Stoppzustand. Das heiĂźt, dass der Thread jthr
in allen Zeilen auĂźer (1) und (2) Stoppanfragen annehmen kann.
Wer den Codeschnipsel sorgfältig studiert, dem fällt wohl std::jthread
auf. std::jthread
in C++20 ist ein erweiterter std::thread
aus C++11. Das "j" in jthread
steht fĂĽr joinable, denn ein std::jthread
joint automatisch in seinem Destruktor. UrsprĂĽnglich hieĂź dieser neue Thread ithread
: "i" steht fĂĽr interruptable. Ich stelle std::jthread
im nächsten Artikel genauer vor.
Das nächste Beispiel zeigt, wie sich std::jthread
zusammen mit einem Callback verwenden lässt:
// 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';
}
Jeder der zehn Threads ruft die Lambda-Funktion func
(1) auf. Der Callback (2) stellt die ID des Threads und den Zähler dar. Dank des einsekundigen Schlafens des main
-Threads (3) und des Schlafens der Kinder-Threads besitzt der Zähler zum Zeitpunkt des Callback-Aufrufs den Wert 4. Der Aufruf thr.request_stop()
(4) startet den Callback auf jedem Thread.
(rme)