Der Erzeuger muss sich um sein Kind kümmern. Diese einfache Aussage besitzt für Threads weitreichende Konsequenzen. Das kleine Programm startet einen Thread, der seine ID auf der Konsole ausgeben soll.
#include <iostream>
#include <thread>
int main(){
std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});
}
Wird das Programm ausgeführt, liefert es leider nicht das gewünschte Ergebnis.
Was ist der Grund?
join und detach
Die Ausführungseinheit des erzeugten Threads endet mit dem Ende der aufrufbaren Einheit. Entweder wartet der Erzeuger, bis sein Kind t fertig ist (t.join()), oder er trennt sich explizit von diesem: t.detach(). Ein Thread t mit Ausführungseinheit (es lassen sich auch Threads ohne ausführbare Einheit erzeugen) ist joinable, wenn auf diesem noch nicht t.join() oder t.detach() aufgerufen wurde. Ein joinable Thread t ruft in seinem Destruktor die Ausnahme std::terminate auf, die zum Abbruch des Programms führt. Genau dies ist der Grund dafür, das die aktuelle Programmausführung zu einer Ausnahme führte.
Die Lösung des Problems ist naheliegend. Durch einen t.join() Aufruf verhält sich das Programm wohldefiniert.
#include <iostream>
#include <thread>
int main(){
std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});
t.join();
}
Exkurs: Die Herausforderungen von detach
Natürlich bietet es sich an, statt t.join() t.detach() zu verwenden. Damit ist der Thread t nicht mehr joinable und sein Destruktor wirft keine std::terminate Ausnahme mehr. Leider handeln wir uns dadurch ein Lebenszeitproblem des Objekts std::cout ein. Die Ausgabe des Programmlaufs ist nicht besonders vielsagend.
Dazu aber im nächsten Artikel mehr.
Threads verschieben
Die Anwendung von join war bisher sehr leicht. Leider trifft dies nicht immer zu.
Threads können nicht kopiert (Copy-Semantik), sondern nur verschoben (Move-Semantik) werden. Wird ein Thread verschoben, ist es deutlich anspruchsvoller für den Erzeuger sich um die Lebenszeit seines Kindes zu kümmern.
#include <iostream>
#include <thread>
#include <utility>
int main(){
std::thread t([]{std::cout << std::this_thread::get_id();});
std::thread t2([]{std::cout << std::this_thread::get_id();});
t= std::move(t2);
t.join();
t2.join();
}
Die zwei Threads t1 und t2 sollen in dem Programm ihre ID ausgeben. Darüber hinaus wird der Thread t2 nach t verschoben: t= std::move(t2). Zum Schluss kümmert sich der main-Thread um die Lebenszeit seiner Kinder, indem er sie joined. Leider deckt sich die Ausgabe des Programms nicht mit meinen Erwartungen.
Was läuft hier schief? Zwei Dinge:
- Durch das Verschieben des Threads t2 erhält der Thread t eine neue Ausführungseinheit und sein Destruktor wird aufgerufen. Dies führt dazu, dass der Destruktor des Threads t std::terminate ausführt, da dieser noch joinable ist.
- Der Thread t2 besitzt durch das Verschieben keine Ausführungseinheit mehr. Der Aufruf join auf einem Thread ohne Ausführungseinheit verursacht eine Ausnahme std::system_error.
Beide Fehler sind nun aber behoben.
#include <iostream>
#include <thread>
#include <utility>
int main(){
std::thread t([]{std::cout << std::this_thread::get_id() << std::endl;});
std::thread t2([]{std::cout << std::this_thread::get_id() << std::endl;});
t.join();
t= std::move(t2);
t.join();
std::cout << "\n";
std::cout << std::boolalpha << "t2.joinable(): " << t2.joinable() << std::endl;
}
Konsequenterweise ist der Thread t2 nicht mehr joinable.
scoped_thread
Wem das explizite sich Kümmern um die Lebenszeit des Kinderthreads zu viel Aufwand ist, der kann ein std::thread in einer eigenen Thread Klasse kapseln, die automatisch join im Destruktor aufruft. Ein automatischer Aufruf von detach im Destruktor ist natürlich auch möglich. Diesen detach-Aufruf im Destruktor als Standardverhalten zu verwenden, bringt aber viele neue Herausforderungen mit sich.
Die Klasse scoped_thread geht auf Anthony Williams zurück. Diese Klasse stellt im Konstruktor sicher, dass der Thread joinable ist und ruft join im Destruktor auf. Da der Copy-Konstruktor und Copy-Zuweisungsoperator als delete deklariert sind, lassen sich Instanzen dieser Klasse weder kopieren noch zuweisen.
#include <iostream>
#include <thread>
#include <utility>
class scoped_thread{
std::thread t;
public:
explicit scoped_thread(std::thread t_): t(std::move(t_)){
if ( !t.joinable()) throw std::logic_error("No thread");
}
~scoped_thread(){
t.join();
}
scoped_thread(scoped_thread&)= delete;
scoped_thread& operator=(scoped_thread const &)= delete;
};
int main(){
scoped_thread t(std::thread([]{std::cout << std::this_thread::get_id() << std::endl;}));
}
In nächsten Artikel geht ich auf die Datenübergabe an Threads ein.
Hintergrundwissen
- Move-Semantik
- Die Details zur Move-Semantik lassen sich in dem Linux-Magazin Artikel Rasch verschoben (12/2012) nachlesen.
- Anthony Williams
- Ist der Maintainer der Boost Thread Bibliothek und Autor des Standardwerkes zu Multithreading in modernem C++: C++ Concurrency in Action.
-
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...