zurück zum Artikel

Softwareentwicklung: Kooperatives Unterbrechen eines Threads in C++20

Rainer Grimm
Viele Eisenbahnschienen vor Sonnenuntergang

(Bild: Patrick Poendl/Shutterstock.com)

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.

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.

Modernes C++ – Rainer Grimm
Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tÀtig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und hÀufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschÀftigt er sich intensiv mit seiner Leidenschaft C++.

Kooperatives Unterbrechen eines Threads in C++20

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:

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.

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_token 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:

Kooperatives Unterbrechen eines Threads in C++20

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:

Kooperatives Unterbrechen eines Threads in C++20

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.

Kooperatives Unterbrechen eines Threads in C++20

(rme [1])


URL dieses Artikels:
https://www.heise.de/-9784410

Links in diesem Artikel:
[1] mailto:rme@ix.de