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.

(Bild: Patrick Poendl/Shutterstock.com)
- Rainer Grimm
Zur Erinnerung: In meinem vorherigen Artikel "Softwareentwicklung: Kooperatives Unterbrechen eines Threads in C++20" habe ich das folgende Programm vorgestellt.
// 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:
Wo wird der Callback ausgefĂĽhrt?
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.
Mehr als ein Callback
// 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';
}
Ein allgemeiner Mechanismus zum Senden von Signalen
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".
Wie geht's weiter?
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)