Lebenszeit von Threads

Inhaltsverzeichnis[Anzeigen]

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.

 threadForgetJoin

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(); 

}

 

threadJoin

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. 

threadDetach

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.

 threadMove

Was läuft hier schief? Zwei Dinge:

  1. 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.
  2. 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.

threadMoveRight

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 Desstruktor 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.

 

 

 

 

 

 

title page smalltitle page small 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.

 

Kommentar schreiben


Modernes C++

Abonniere den Newsletter

Inklusive zwei Artikel meines Buchs
Introduction und Multithreading

Beiträge-Archiv

Sourcecode

Neuste Kommentare