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.

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.