Ein verbesserter Thread mit C++20
std::jthread steht für einen automatisch joinenden Thread. Im Gegensatz zu std::thread (C++11) joint std::jthread automatisch in seinem Destruktor und kann kooperativ unterbrochen werden. Dieser Artikel zeigt, warum std::jthread die erste Wahl sein sollte.
std::jthread
steht für einen automatisch joinenden Thread. Im Gegensatz zu std::thread
(C++11) joint std::jthread
automatisch in seinem Destruktor und kann kooperativ unterbrochen werden. Dieser Artikel zeigt, warum std::jthread
die erste Wahl sein sollte.
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/TimelineCpp20-0cbc7373eff3d2f0.png)
Die folgende Tabelle gibt den ersten kompakten Überblick des Interfaces eines std::jthread
.
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/jthread-91ecf5b74160997f.png)
Weitere Details liefert wie immer cppreference.com [1]. Die Details zu std::thread
lassen sich auf meinem Blog nachlesen: Meine Artikel zu std::thread [2].
Warum benötigen wir einen verbesserten Thread in C++20?
Automatisch joinen
Hier ist das nicht intuitive Verhalten des std::thread
. Wenn ein std::thread
noch joinable ist, wird automatisch std::terminate
in seinem Destruktor aufgerufen. Ein Thread thr
ist joinable, wenn auf ihm noch nicht thr.join()
oder thr.detach()
ausgeführt wurde:
// threadJoinable.cpp
#include <iostream>
#include <thread>
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
std::thread thr{[]{ std::cout << "Joinable std::thread" << '\n'; }};
std::cout << "thr.joinable(): " << thr.joinable() << '\n';
std::cout << '\n';
}
Wird das Programm ausgeführt, beendet es sich abrupt, wenn das lokale Objekt thr
seinen Gültigkeitsbereich verliert.
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/644/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/threadJoinable-69f95b8e14f4d0ab.png)
Beide Threads beenden sich abrupt. Im zweiten Fall besitzt der Thread noch genügend Zeit, seine Nachricht auszugeben: Joinable std::thread
.
In meinem nächsten Beispiel verwende ich std::jthread
aus dem C++20-Standard:
// jthreadJoinable.cpp
#include <iostream>
#include <thread>
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
std::jthread thr{[]{ std::cout << "Joinable std::thread" << '\n'; }};
std::cout << "thr.joinable(): " << thr.joinable() << '\n';
std::cout << '\n';
}
Nun ruft der Thread thr
automatisch join
in seinem Destruktor auf, wenn er wie in diesem Fall noch joinable ist.
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/443/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/jthreadJoinable-4c13b6e77fb54326.png)
Das ist noch nicht alles, was ein std::jthread
zusätzlich zu einem std::thread
anbietet. Ein std::jthread
lässt sich auch kooperativ unterbrechen. Ich habe bereits in meinem letzten Artikel die Idee des kooperativen Unterbrechens vorgestellt [3].
Das folgende Programm stellt das Unterbrechen eines std::jthread
genauer vor:
// interruptJthread.cpp
#include <chrono>
#include <iostream>
#include <thread>
using namespace::std::literals;
int main() {
std::cout << '\n';
std::jthread nonInterruptable([]{ // (1)
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
std::cerr << "nonInterruptable: " << counter << '\n';
++counter;
}
});
std::jthread interruptable([](std::stop_token stoken){ // (2)
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
if (stoken.stop_requested()) return; // (3)
std::cerr << "interruptable: " << counter << '\n';
++counter;
}
});
std::this_thread::sleep_for(1s);
std::cerr << '\n';
std::cerr << "Main thread interrupts both jthreads" << '\n';
nonInterruptable.request_stop();
interruptable.request_stop(); // (4)
std::cout << '\n';
}
Ich starte im main
-Programm die zwei Threads nonInterruptable
und interruptable
(Zeilen 1 und 2). Im Gegensatz zum Thread nonInterruptable
erhält der Thread interruptable
ein std::stop_token
und verwendet diesen, um in Zeile (3) zu prüfen, ob er unterbrochen wurde: stoken.stop_requested()
. Im Fall einer Unterbrechung wird die Lambda-Funktion einfach beendet, sodass der Thread mit seiner Ausführung fertig ist. Der Aufruf interruptable.request_stop()
in Zeile (4) stößt die Beendigung des Threads an. Dies gilt nicht für den vorherigen Aufruf nonInterruptable.request_stop()
, der keinen Effekt besitzt.
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/interruptJthread-35488ccadc8d2b07.png)
Um den Artikel schön abzuschließen, zeige ich, wie sich in C++20 auch eine Bedingungsvariable kooperativ unterbrechen lässt.
Neue wait-Überladungen für std::condition_variable_any
Bevor ich genauer auf std::condition_variable_any
eingehe, möchte ich auf meine Artikel zu Bedingungsvariablen verweisen: Bedingungsvariablen [4].
Die wait-Varianten wait
, wait_for
und wait_until
der std::condition_variable_any
erhalten neue Überladungen. Diese können ein std::stop_token
annehmen:
template <class Predicate>
bool wait(Lock& lock,
stop_token stoken,
Predicate pred);
template <class Rep, class Period, class Predicate>
bool wait_for(Lock& lock,
stop_token stoken,
const chrono::duration<Rep, Period>& rel_time,
Predicate pred);
template <class Clock, class Duration, class Predicate>
bool wait_until(Lock& lock,
stop_token stoken,
const chrono::time_point<Clock, Duration>& abs_time,
Predicate pred);
Die neuen Überladungen benötigen ein Prädikat. Die Varianten stellen sicher, benachrichtigt zu werden, wenn eine Unterbrechung an den übergebenen std::interrupt_token stoken
geschickt wurde. Nach dem wait
-Aufruf lässt sich dann prüfen, ob eine Unterbrechung vorliegt:
cv.wait(lock, stoken, predicate);
if (stoken.is_interrupted()){
// interrupt occurred
}
Das folgende Beispiel stellt die Anwendung einer Bedingungsvariable mit einer Stopp-Aufforderung vor.
// conditionVariableAny.cpp
#include <condition_variable>
#include <thread>
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
using namespace std::literals;
std::mutex mutex_;
std::condition_variable_any condVar;
bool dataReady;
void receiver(std::stop_token stopToken) { // (1)
std::cout << "Waiting" << '\n';
std::unique_lock<std::mutex> lck(mutex_);
bool ret = condVar.wait(lck, stopToken, []{return dataReady;});
if (ret){
std::cout << "Notification received: " << '\n';
}
else{
std::cout << "Stop request received" << '\n';
}
}
void sender() { // (2)
std::this_thread::sleep_for(5ms);
{
std::lock_guard<std::mutex> lck(mutex_);
dataReady = true;
std::cout << "Send notification" << '\n';
}
condVar.notify_one(); // (3)
}
int main(){
std::cout << '\n';
std::jthread t1(receiver);
std::jthread t2(sender);
t1.request_stop(); // (4)
t1.join();
t2.join();
std::cout << '\n';
}
Der Empfänger-Thread (Zeile 1) wartet auf die Benachrichtigung des Sender-Threads (Zeile 2). Bevor der Sender seine Benachrichtigung schickt (Zeile 3), stößt der main-
Thread eine Stopp-Anfrage an (Zeile 4). Die Ausgabe des Programms zeigt, dass die Stopp-Anforderung vor der Benachrichtigung stattfindet:
![Ein verbesserter Thread mit C++20](https://heise.cloudimg.io/width/281/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/0/5/6/1/2/0/conditionVariableAny-c9ceb823525af98d.png)
Wie geht's weiter
Was kann passieren, wenn du unsynchroniziert auf std::cout
schreibst? Du erhältst ein Durcheinander. Dank C++20 lassen sich auch synchronisierte Ausgabestreams einsetzen.
( [5])
URL dieses Artikels:
https://www.heise.de/-5054205
Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/thread/jthread
[2] https://www.grimm-jaud.de/index.php/der-einstieg-in-modernes-c#h1-2-standardisierte-threading-schnittstelle
[3] https://heise.de/-5047812
[4] https://www.grimm-jaud.de/index.php/blog/tag/bedingungsvariablen
[5] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien