Ein neuer Thread mit C++20: std::jthread

Modernes C++  –  0 Kommentare

Ein Teilnehmer meines CppCon-2018-Workshops fragte mich: "Kann ein Thread unterbrochen werden?" Nein, war meine Antwort, doch dies ist nicht mehr richtig. Mit C++20 werden wir wohl std::jthread erhalten.

Gerne möchte ich meine Geschichte von der CppCon 2018 fortsetzen. Während einer Pause meines Concurrency-Workshops hatte ich ein kurzes Gespräch mit Nicolai (Josuttis). Er fragte mich, was ich über das Proposal "P0660: Cooperatively Interruptible Joining Thread" denke. Zu diesem Zeitpunkt war mir das Proposal noch nicht bekannt. Nicolai ist zusammen mit Herb Sutter und Anthony Williams einer seiner Autoren. Heute geht es um die Concurrent-Zukunft. Hier ist der erste Überblick zur Concurrency in aktuellem und zukünftigem C++.

Aufgrund des Titel des Artikels "Cooperatively Interruptible Joining Thread" ahnst du es vermutlich schon. Der neue Thread besitzt zwei zusätzliche Fähigkeiten. Er ist unterbrechbar und ruft join automatisch auf. Zuerst will ich mich mit der zweiten Verbesserung befassen.

Automatischer join-Aufruf

Hier ist das nicht so 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 << std::endl;
std::cout << std::boolalpha;

std::thread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};

std::cout << "thr.joinable(): " << thr.joinable() << std::endl;

std::cout << std::endl;

}

Wenn das Programm ausgeführt wird, beendet es sich abrupt.

Beide Threads beenden sich abrupt. Im zweiten Fall besitzt der Thread noch genügend Zeit, seine Nachricht auszugeben: "Joinable std::thread".

Im nächsten Beispiel ersetze ich den Header <thread> mit dem Header "jthread.hpp" und verwende den std::jthread aus dem zukünftigen C++-Standard:

// jthreadJoinable.cpp

#include <iostream>
#include "jthread.hpp"

int main(){

std::cout << std::endl;
std::cout << std::boolalpha;

std::jthread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};

std::cout << "thr.joinable(): " << thr.joinable() << std::endl;

std::cout << std::endl;

}

Nun ruft der Thread thr automatisch join in seinem Destruktor auf, wenn er noch joinable ist.

Unterbrechen eines std::jthread

Das folgende Beispiel stellt die Unterbrechung eines std::jthread genauer vor:

// interruptJthread.cpp

#include "jthread.hpp"
#include <chrono>
#include <iostream>

using namespace::std::literals;

int main(){

std::cout << std::endl;

std::jthread nonInterruptable([]{ // (1)
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
std::cerr << "nonInterruptable: " << counter << std::endl;
++counter;
}
});

std::jthread interruptable([](std::interrupt_token itoken){ // (2)
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
if (itoken.is_interrupted()) return; // (3)
std::cerr << "interruptable: " << counter << std::endl;
++counter;
}
});

std::this_thread::sleep_for(1s);

std::cerr << std::endl;
std::cerr << "Main thread interrupts both jthreads" << std:: endl;
nonInterruptable.interrupt();
interruptable.interrupt(); // (4)

std::cout << std::endl;

}

Ich starte im main-Programm die zwei Threads nonInterruptable und interruptable (Zeilen 1 and 2). Im Gegensatz zum Thread nonInterruptable erhält der Thread interruptable ein std::interrupt_token und verwendet diesen, um in Zeile 3 zu prüfen, ob er unterbrochen wurde: itoken.is_interrupted(). Im Fall einer Unterbrechung wird die Lambda-Funktion einfach beendet, sodass der Thread mit seiner Ausführung fertig ist. Der Aufruf interruptable.interrupt() in Zeile 4 stößt die Beendigung des Threads an. Dies gilt nicht für den vorherigen Aufruf nonInterruptable.interrupt(), der keinen Effekt besitzt.

Jetzt gehe ich auf die weiteren Details zu Interrupt Tokens, Joining Threads und Bedingungsvariablen ein.

Interrupt Tokens

Ein Interrupt Token std::interrupt_token bietet geteilte Besitzverhältnisse an und kann dazu verwendet werden, einmalig ein Signal zu schicken, wenn der Token gültig ist. Es besitzt die drei Methoden valid, is_interrupted und interrupt.

Wenn der Interrupt Token temporär disabled werden soll, kannst du ihm mit einem per Default erzeugten Interrupt Token austauschen. Ein Default erzeugtes Interrupt Token ist nicht gültig. Der folgende Codeschnipsel zeigt, wie die Fähigkeit, einen Thread zu unterbrechen, disabled und enabled werden kann:

std::jthread jthr([](std::interrupt_token itoken){
...
std::interrupt_token interruptDisabled;
std::swap(itoken, interruptDisabled); // (1)
...
std::swap(itoken, interruptDisabled); // (2)
...
}

std::interrupt_token interruptDisabled ist nicht gültig. Das heißt, dass der Thread von Zeile (1) bis (2) keine Unterbrechung annehmen kann. Ab Zeile (2) ist es wieder möglich.

std::jthread

Ein std::jthread ist ein std::thread mit der zusätzlichen Möglichkeit, eine Unterbrechung zu schicken und automatisch in seinem Destruktor join() auszuführen. Um diese Funktionalität anzubieten, besitzt er einen std::interrupt_token.

Neue wait-Überladungen für Bedingungsvariablen.

Die zwei wait-Varianten wait_for und wait_until der std::condition_variable erhält neue Überladungen. Diese können ein std::interrupt_token annehmen:

template <class Predicate>
bool wait_until(unique_lock<mutex>& lock,
Predicate pred,
interrupt_token itoken);

template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lock,
const chrono::duration<Rep, Period>& rel_time,
Predicate pred,
interrupt_token itoken);

template <class Clock, class Duration, class Predicate>
bool wait_until(unique_lock<mutex>& lock,
const chrono::time_point<Clock, Duration>& abs_time,
Predicate pred,
interrupt_token itoken);

Die neuen Überladungen benötigen ein Prädikat. Die Varianten stellen sicher, benachrichtigt zu werden, wenn eine Unterbrechung an den übergebenen std::interrupt_token itoken geschickt wurde. Nach dem wait-Aufruf lässt sich dann prüfen, ob eine Unterbrechung vorliegt:

cv.wait_until(lock, predicate, itoken);
if (itoken.is_interrupted()){
// interrupt occurred
}

Wie geht's weiter?

Wie ich bereits in meinem letzten Artikel angekündigt habe, geht es in meinen nächsten Artikel um die verbleibenden Regeln zur Definition von Concepts.

Meine Schulung: Multithreading mit modernem C++

Am 12. und 13. November dieses Jahres halte ich denselben Workshop, den ich auf der CppCon 2018 gehalten haben, als Schulung in deutscher Sprache in Rottenburg. Es sind noch Plätze frei und ich freue mich auf diese anspruchsvolle Schulung.

Hier gibt es mehr Details: Multithreading mit modernem C++.