C++20: Kooperative Unterbrechung eines Thread mit Callbacks​

Nach der grundsätzlichen Vorstellung zum kooperativen Unterbrechen eines Thread geht dieser Beitrag auf die Details ein.

In Pocket speichern vorlesen Druckansicht
Viele Eisenbahnschienen vor Sonnenuntergang

(Bild: Patrick Poendl/Shutterstock.com)

Lesezeit: 3 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Zur Erinnerung: In meinem vorherigen Artikel "Softwareentwicklung: Kooperatives Unterbrechen eines Threads in C++20" habe ich das folgende Programm vorgestellt.

Modernes C++ – 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++.

// 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) zeigt die Thread-ID und den Zähler an. Da der Haupt-Thread (3) eine Sekunde schläft und die untergeordneten Threads schlafen, ist der Zähler 4, wenn die Callbacks aufgerufen werden. Der Aufruf thr.request_stop() löst den Callback auf jedem Thread aus.

Eine Frage wurde in meinem letzten Artikel nicht beantwortet:

Der std::stop_callback-Konstruktor registriert die Callback-Funktion für das std::stop_token, das durch die zugehörige std::stop_source geben ist. Diese Callback-Funktion wird entweder in dem Thread aufgerufen, der request_stop() aufruft, oder in dem Thread, der den std::stop_callback konstruiert. Wenn die Aufforderung zum Anhalten vor der Registrierung des std::stop_callback erfolgt, wird der Callback in dem Thread aufgerufen, der den std::stop_callback konstruiert. Andernfalls wird der Callback in dem Thread aufgerufen, der request_stop aufruft. Erfolgt der Aufruf request_stop() nach der Ausführung des Threads, der den std::stop_callback konstruiert, wird der registrierte Callback nie aufgerufen.

Man kann mehr als einen Callback für einen oder mehrere Threads mit demselben std::stop_token registrieren. Der C++-Standard bietet keine Garantie für die Reihenfolge, in der sie ausgeführt werden.

// invokeCallbacks.cpp

#include <chrono>
#include <iostream>
#include <thread>

using namespace std::literals;

void func(std::stop_token stopToken) {
    std::this_thread::sleep_for(100ms);
    for (int i = 0; i <= 9; ++i) {
       std::stop_callback cb(stopToken, [i] { std::cout << i; });
    }
    std::cout << '\n';
}

int main() {
    
    std::cout << '\n';
    
    std::jthread thr1 = std::jthread(func);
    std::jthread thr2 = std::jthread(func);
    thr1.request_stop();
    thr2.request_stop();

    std::cout << '\n';
    
}

Das Paar std::stop_source und std::stop_token kann als allgemeiner Mechanismus zum Senden eines Signals betrachtet werden. Indem man das std::stop_token kopiert, kann man das Signal an jede Entität senden, die etwas ausführt. Im folgenden Beispiel verwende ich std::async, std::promise, std::thread und std::jthread in verschiedenen Kombinationen.

// signalStopRequests.cpp

#include <iostream>
#include <thread>
#include <future>

using namespace std::literals;

void function1(std::stop_token stopToken, const std::string& str){
  std::this_thread::sleep_for(1s);
  if (stopToken.stop_requested()) std::cout << str 
                                            << ": Stop requested\n";
}

void function2(std::promise<void> prom, 
               std::stop_token stopToken, const std::string& str) {
  std::this_thread::sleep_for(1s);
  std::stop_callback callBack(stopToken, [&str] { 
    std::cout << str << ": Stop requested\n"; 
  });
  prom.set_value();
}

int main() {

  std::cout << '\n';

  std::stop_source stopSource;                          // (1)

  std::stop_token stopToken = 
    std::stop_token(stopSource.get_token());            // (2)

  std::thread thr1 = 
    std::thread(function1, stopToken, "std::thread");   // (3)
    
  std::jthread jthr = 
    std::jthread(function1, stopToken, "std::jthread"); // (4)
    
  auto fut1 = std::async([stopToken] {                  // (5)
    std::this_thread::sleep_for(1s);
    if (stopToken.stop_requested()) std::cout 
      << "std::async: Stop requested\n";
  });

  std::promise<void> prom;                              // (6)
  auto fut2 = prom.get_future();
  std::thread thr2(function2, std::move(prom), 
                   stopToken, "std::promise");

  stopSource.request_stop();                            // (7)
  if (stopToken.stop_requested()) 
    std::cout << "main: Stop requested\n";              // (8)

  thr1.join();
  thr2.join();

  std::cout << '\n';

}

Dank der stopSource (1) kann ich das stopToken (2) für jede laufende Entität verwenden, z. B. std::thread (3), std::jthread (4), std::async (5) oder std::promise (6). Ein std::stop_token ist billig zu kopieren. (7) löst stopSource.request_stop aus. Außerdem erhält der Haupt-Thread (8) das Signal. Ich verwende in diesem Beispiel std::jthread. std::jthread und std::condition_variable_any haben explizite Mitgliedsfunktionen, um mit kooperativen Unterbrechungen bequemer umgehen zu können. Mehr steht im Artikel "Ein verbesserter Thread mit C++20".

In den nächsten zwei Wochen werde ich eine Schreibpause einlegen. Danach werde ich wieder in C++23 einsteigen und das erste Mal in C++26 nachlegen. (rme)