Die Boost C++ Bibliotheken

Threads erstellen und verwalten

Die wichtigste Klasse in Boost.Thread ist boost::thread. Sie ist in der Headerdatei boost/thread.hpp definiert und wird verwendet, um einen Thread zu erstellen. Im Beispiel 44.1 sehen Sie, wie sie angewandt wird.

Beispiel 44.1. boost::thread in Aktion
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

void thread()
{
  for (int i = 0; i < 5; ++i)
  {
    wait(1);
    std::cout << i << '\n';
  }
}

int main()
{
  boost::thread t{thread};
  t.join();
}

Dem Konstruktor von boost::thread wird der Name der Funktion übergeben, die als Thread gestartet werden soll. Wenn im Beispiel 44.1 die Variable t erstellt wird, beginnt die Funktion thread() sofort, in einem eigenen Thread zu laufen. Die Funktion thread() wird ab diesem Zeitpunkt gleichzeitig zur Funktion main() ausgeführt.

Weil das Programm wie gewohnt nach Ablauf der Funktion main() beendet werden würde, wird für den soeben gestarteten Thread join() aufgerufen. Es handelt sich dabei um eine blockierende Methode: join() hält den aktuellen Thread an und kehrt erst dann zurück, wenn der Thread, für den join() aufgerufen wurde, beendet wurde. Dies bedeutet für Beispiel 44.1, dass main() solange wartet, bis thread() seine Arbeit getan hat und zurückkehrt.

Beachten Sie, dass Sie über eine Variable wie t auf einen Thread zugreifen können, um zum Beispiel mit join() auf dessen Beendigung zu warten. Der Thread würde aber auch dann weiterlaufen, wenn der Gültigkeitsbereich von t enden und die Variable zerstört werden würde. Ein Thread ist anfangs an eine Variable vom Typ boost::thread gebunden, läuft aber auch dann weiter, wenn die Variable nicht mehr existiert. Sie können sogar eine Methode detach() aufrufen, mit der Sie eine Variable vom Typ boost::thread von einem Thread entkoppeln können. Danach können Sie zum Beispiel nicht mehr join() aufrufen, weil die Variable keinen Thread mehr repräsentiert.

Sie können in einem Thread grundsätzlich all das machen, was Sie in jeder beliebigen Funktion machen können. Letztendlich ist der Thread nichts anderes als eine Funktion – mit der Besonderheit, dass sie zeitgleich zu anderen Funktionen ausgeführt wird. Im Beispiel 44.1 werden in einer Schleife fünf Zahlen auf die Standardausgabe ausgegeben. Damit die Datenausgabe nicht zu schnell erfolgt, wird in jedem Schleifendurchgang die Funktion wait() aufgerufen, mit der die Ausführung des aktuellen Threads für jeweils eine Sekunde angehalten wird. Die Funktion wait() greift dazu auf eine freistehende Funktion namens sleep_for() zu, die ebenfalls von Boost.Thread stammt und im Namensraum boost::this_thread definiert ist.

Sie müssen sleep_for() angeben, für wie lange der aktuelle Thread angehalten werden soll. Indem ein Objekt vom Typ boost::chrono::seconds übergeben wird, wird eine Zeitspanne definiert, für die der aktuelle Thread angehalten wird. boost::chrono::seconds stammt aus der Bibliothek Boost.Chrono, die im Kapitel 37 vorgestellt wird.

Beachten Sie, dass sleep_for() ausschließlich Zeitangaben aus der Bibliothek Boost.Chrono akzeptiert. Auch wenn Boost.Chrono in die C++11-Standardbibliothek eingegangen ist – Zeitangaben aus dem Namensraum std::chrono werden nicht akzeptiert und führen zu Compiler-Fehlern.

Möchten Sie join() nicht explizit aufrufen, um am Ende der Funktion main() auf den Thread zu warten, können Sie die Klasse boost::scoped_thread verwenden.

Beispiel 44.2. Mit boost::scoped_thread auf einen Thread warten
#include <boost/thread.hpp>
#include <boost/thread/scoped_thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

void thread()
{
  for (int i = 0; i < 5; ++i)
  {
    wait(1);
    std::cout << i << '\n';
  }
}

int main()
{
  boost::scoped_thread<> t{boost::thread{thread}};
}

Sie übergeben an den Konstruktor von boost::scoped_thread einen Thread in Form einer Instanz von boost::thread. Der Thread wird wie im Beispiel zuvor gestartet. Die Besonderheit von boost::scoped_thread liegt darin, dass diese Klasse im Destruktor eine Aktion ausführt, in der auf den Thread zugegriffen werden kann. Standardmäßig ruft boost::scoped_thread für den Thread join() auf. Somit verhält sich Beispiel 44.2 wie das vorherige.

Sie können eigene Aktionen als Template-Parameter übergeben. Dabei muss es sich um eine Klasse handeln, die den Operator operator() derart überlädt, dass er einen Parameter vom Typ boost::thread erwartet. boost::scoped_thread garantiert, dass der Operator im Destruktor aufgerufen wird.

boost::scoped_thread ist eine Klasse, die Sie ausschließlich in Boost.Thread finden. Es gibt in der Standardbibliothek kein Gegenstück. Beachten Sie, dass Sie für boost::scoped_thread jedoch zusätzlich die Headerdatei boost/thread/scoped_thread.hpp einbinden müssen.

Im Folgenden lernen Sie Unterbrechungspunkte kennen, mit Hilfe derer ein Thread einen anderen unterbrechen kann. Unterbrechungspunkte werden ebenfalls aussschließlich von Boost.Thread unterstützt und fehlen in der Standardbibliothek.

Beispiel 44.3. Ein Unterbrechungspunkt mit boost::this_thread::sleep_for()
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

void thread()
{
  try
  {
    for (int i = 0; i < 5; ++i)
    {
      wait(1);
      std::cout << i << '\n';
    }
  }
  catch (boost::thread_interrupted&) {}
}

int main()
{
  boost::thread t{thread};
  wait(3);
  t.interrupt();
  t.join();
}

Wenn Sie die Methode interrupt() aufrufen, wird der entsprechende Thread unterbrochen. Unterbrochen bedeutet, dass im Thread eine Ausnahme vom Typ boost::thread_interrupted geworfen wird. Dies geschieht jedoch nur dann, wenn der Thread einen Unterbrechungspunkt erreicht.

Der alleinige Aufruf von interrupt() bewirkt nichts, wenn ein Thread keine Unterbrechungspunkte enthält. Beim Aufruf von interrupt() wird also nicht sofort eine Ausnahme vom Typ boost::thread_interrupted geworfen. Stattdessen überprüft der Thread in den bereits erwähnten Unterbrechungspunkten, ob interrupt() aufgerufen wurde – ist dies der Fall, wird eine Ausnahme vom Typ boost::thread_interrupted geworfen.

Boost.Thread definiert eine Reihe von Unterbrechungspunkten. Ein Unterbrechungspunkt ist zum Beispiel sleep_for(). Da sleep_for() aufgrund der Schleife in thread() fünfmal aufgerufen wird, überprüft der Thread fünfmal, ob er unterbrochen wurde. Zwischen den Aufrufen von sleep_for() kann der Thread im Beispiel 44.3 nicht unterbrochen werden, und es wird keine Ausnahme vom Typ boost::thread_interrupted geworfen.

Wenn Sie Beispiel 44.3 ausführen, stellen Sie fest, dass nicht mehr alle fünf Zahlen ausgegeben werden. Der Grund ist, dass in der Funktion main() nach drei Sekunden interrupt() aufgerufen wird. Damit wird thread() nach drei Sekunden unterbrochen, und es wird eine Ausnahme vom Typ boost::thread_interrupted geworfen. Diese wird zwar im Thread abgefangen, ohne dass etwas im catch-Block geschieht. Weil jedoch anschließend die Funktion thread() zurückkehrt, endet der Thread – und damit das Programm, da main() lediglich mit join() auf das Ende des Threads wartet.

Boost.Thread definiert rund fünfzehn Unterbrechungspunkte, zu denen wie eben gesehen die Funktion sleep_for() zählt. Dank dieser Unterbrechungspunkte lassen sich relativ einfach und zeitnah Threads unterbrechen. Dadurch, dass immer erst ein Unterbrechungspunkt erreicht werden muss, um zu überprüfen, ob eine Ausnahme vom Typ boost::thread_interrupted geworfen werden muss, sind Unterbrechungspunkte aber nicht zwangsläufig die jeweils beste Wahl.

Beispiel 44.4. Unterbrechungspunkte mit disable_interruption deaktivieren
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

void thread()
{
  boost::this_thread::disable_interruption no_interruption;
  try
  {
    for (int i = 0; i < 5; ++i)
    {
      wait(1);
      std::cout << i << '\n';
    }
  }
  catch (boost::thread_interrupted&) {}
}

int main()
{
  boost::thread t{thread};
  wait(3);
  t.interrupt();
  t.join();
}

Mit boost::this_thread::disable_interruption bietet Boost.Thread eine Klasse an, mit der verhindert werden kann, dass ein Thread unterbrochen wird. Wenn Sie eine Instanz dieser Klasse erstellen, sind Unterbrechungspunkte im aktuellen Thread so lange deaktiviert, so lange die Instanz existiert. Beispiel 44.4 gibt dementsprechend fünf Zahlen aus, da der Versuch, den Thread zu unterbrechen, ignoriert wird.

Beispiel 44.5. Thread-Eigenschaften über boost::thread::attributes setzen
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

void thread()
{

  try
  {
    for (int i = 0; i < 5; ++i)
    {
      wait(1);
      std::cout << i << '\n';
    }
  }
  catch (boost::thread_interrupted&) {}
}

int main()
{
  boost::thread::attributes attrs;
  attrs.set_stack_size(1024);
  boost::thread t{attrs, thread};
  t.join();
}

Über boost::thread::attributes ist es möglich, Thread-Eigenschaften zu setzen. Genaugenommen kann jedoch nur eine einzige Thread-Eigenschaft portabel gesetzt werden, nämlich die Größe des Stacks. Im Beispiel 44.5 wird dieser über boost::thread::attributes::set_stack_size() auf 1024 Bytes gesetzt.

Beispiel 44.6. Thread-ID und Anzahl verfügbarer Prozessoren/Kerne erhalten
#include <boost/thread.hpp>
#include <iostream>

int main()
{
  std::cout << boost::this_thread::get_id() << '\n';
  std::cout << boost::thread::hardware_concurrency() << '\n';
}

Über den Namensraum boost::this_thread können wie im Beispiel 44.6 verschiedene freistehende Funktionen aufgerufen werden, die sich auf den aktuellen Thread beziehen. sleep_for() hatten Sie bereits kennengelernt. Eine weitere Funktionen ist get_id(): Sie gibt eine Nummer zurück, mit der der aktuelle Thread identifiziert werden kann. get_id() steht auch als Methode der Klasse boost::thread zur Verfügung.

Die Methode hardware_concurrency(), die als statische Methode zur Klasse boost::thread gehört, gibt die Zahl der Threads zurück, die tatsächlich dank verfügbarer Prozessoren oder Prozessorkerne gleichzeitig ausgeführt werden können. So wird zum Beispiel auf einem Computer mit Dual-Core-Prozessor der Wert 2 zurückgegeben. Der Rückgabewert erlaubt es einem Programm zum Beispiel, genau so viele Threads zu erstellen, wie von der Hardware, auf der ein Programm läuft, gleichzeitig ausgeführt werden können.

Boost.Thread bietet mit boost::thread_group außerdem eine Klasse an, um Threads in einer Gruppe zusammenzufassen und gemeinsam zu verwalten. So stellt diese Klasse zum Beispiel die Methode join_all() zur Verfügung, um auf alle Threads in der Gruppe zu warten.

Aufgaben

  1. Restrukturieren Sie dieses Programm, um die Summe der Zahlen, die in der for-Schleife verarbeitet werden, mit Hilfe von zwei Threads zu berechnen. Da viele Prozessoren heute zwei Kerne besitzen und sich ein derartiger Mehrkernprozessor möglicherweise auch in Ihrem Computer befindet, wird durch die Verwendung von Threads die Ausführungsgeschwindigkeit des Programms erhöht:

    #include <boost/timer/timer.hpp>
    #include <iostream>
    #include <cstdint>
    
    int main()
    {
        boost::timer::cpu_timer timer;
    
        std::uint64_t total = 0;
        for (int i = 0; i < 1'000'000'000; ++i)
            total += i;
    
        std::cout << timer.format();
        std::cout << total << '\n';
    }
    
  2. Verallgemeinern Sie Ihr Programm, indem jeweils genauso viele Threads verwendet werden wie vom Prozessor gleichzeitig ausgeführt werden können. So sollen zum Beispiel vier Threads verwendet werden, wenn das Programm auf einem Computer mit einem Prozessor mit vier Kernen ausgeführt wird.