Ein verbesserter Thread mit C++20

Modernes C++ Rainer Grimm  –  87 Kommentare

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

Die folgende Tabelle gibt den ersten kompakten Überblick des Interfaces eines std::jthread.

Ein verbesserter Thread mit C++20

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.

Ein verbesserter Thread mit C++20

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

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.

Ein verbesserter Thread mit C++20

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:

Ein verbesserter Thread mit C++20


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.