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.
- Rainer Grimm
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.
Die folgende Tabelle gibt den ersten kompakten Überblick des Interfaces eines std::jthread
.
Weitere Details liefert wie immer cppreference.com. Die Details zu std::thread
lassen sich auf meinem Blog nachlesen: Meine Artikel zu std::thread.
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.
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.
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.
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.
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.
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:
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.