Die Boost C++ Bibliotheken

Kapitel 51. Boost.Coroutine

Mit Boost.Coroutine ist es möglich, Coroutinen in C++ zu verwenden. Dabei handelt es sich um ein Feature, das in anderen Programmiersprachen häufig mit yield in Verbindung gebracht wird. In diesen Programmiersprachen ist es mit yield möglich, ähnlich wie mit return Funktionen zu beenden. Wird yield verwendet, merkt sich eine Funktion, wo sie beendet wurde. Wird die Funktion ein zweites Mal aufgerufen, setzt sie genau dort fort.

C++ kennt kein Schlüsselwort yield. Mit Boost.Coroutine ist es jedoch auch in C++ möglich, Funktionen zu beenden und später dort fortzusetzen, wo sie beendet wurden. Mit Boost.Asio existiert auch eine Boost-Bibliothek, die auf Boost.Coroutine zugreift und von Coroutinen profitiert.

Anmerkung

Boost.Coroutine ist eine relativ neue Bibliothek, die sich häufig ändert und inzwischen in mehreren Versionen vorliegt. Die erste Version erschien mit Boost 1.53.0. Mit Boost 1.55.0 wurde eine zweite Version eingeführt, die die erste Version ersetzte. Diese zweite Version wurde in Boost 1.56.0 erweitert. In diesem Kapitel lernen Sie die zweite Version kennen, wie sie mit Boost 1.55.0 eingeführt wurde. Sie können die Beispiele in diesem Kapitel kompilieren, wenn Sie Boost 1.55.0 oder eine neuere Version verwenden.

Beispiel 51.1. Coroutinen in Aktion
#include <boost/coroutine/all.hpp>
#include <iostream>

using namespace boost::coroutines;

void cooperative(coroutine<void>::push_type &sink)
{
  std::cout << "Hello";
  sink();
  std::cout << "world";
}

int main()
{
  coroutine<void>::pull_type source{cooperative};
  std::cout << ", ";
  source();
  std::cout << "!\n";
}

Beispiel 51.1 enthält neben main() eine Funktion cooperative(). cooperative() soll von main() als Coroutine aufgerufen werden. cooperative() soll daraufhin die Code-Ausführung vorzeitig beenden und zu main() zurückkehren, um dann ein zweites Mal aufgerufen zu werden und an der Stelle fortzusetzen, wo die Funktion vorher beendet wurde.

Um cooperative() als Coroutine verwenden zu können, wird auf zwei Typen pull_type und push_type zugegriffen, die beide von boost::coroutines::coroutine angeboten werden. boost::coroutines::coroutine ist ein Template, das im Beispiel mit void instanziiert wird.

Um Coroutinen verwenden zu können, benötigen Sie pull_type und push_type. Einen dieser Typen verwenden Sie, um ein Objekt zu erstellen, dem Sie die Funktion, die Sie als Coroutine verwenden wollen, als Parameter übergeben. Den anderen Typ verwenden Sie als ersten Parameter in der Funktion, die als Coroutine verwendet werden soll.

Im Beispiel wird in main() ein Objekt source vom Typ pull_type erstellt. Dem Konstruktor wird cooperative() übergeben. push_type wird als einziger Parameter im Funktionskopf von cooperative() verwendet.

Der Konstruktor von pull_type ruft die Funktion, die als Parameter übergeben wird, als Coroutine auf. Im Beispiel wird demnach cooperative() aufgerufen, wenn source erstellt wird. Würde source auf push_type basieren, würde kein automatischer Aufruf stattfinden.

cooperative() gibt Hello auf die Standardausgabe aus. Anschließend greift die Funktion auf den Parameter sink zu, als würde es sich um eine Funktion handeln. Das ist möglich, weil push_type den Operator operator() überlädt. Während source in main() die Coroutine cooperative() repräsentiert, stellt sink in cooperative() die Funktion main() dar. Der Aufruf von sink führt dazu, dass cooperative() beendet und main() dort fortgesetzt wird, von wo cooperative() soeben aufgerufen worden war. In main() wird daraufhin ein Komma auf die Standardausgabe ausgegeben.

main() greift anschließend so auf source zu, als würde es sich um eine Funktion handeln. Das ist möglich, weil auch pull_type den Operator operator() überlädt. Daraufhin wird cooperative() aufgerufen und an der Stelle fortgesetzt, an der die Coroutine vorher beendet wurde. So gibt cooperative() world auf die Standardausgabe aus. Da es keine weiteren Anweisungen in cooperative() gibt, endet die Coroutine. Die Code-Ausführung kehrt zurück zu main(), wo ein Ausrufezeichen auf die Standardausgabe ausgegeben wird.

Beispiel 51.1 gibt zusammengenommen Hello, world! aus.

Sie können sich Coroutines als kooperative Threads vorstellen. In gewisser Weise laufen die Funktionen main() und cooperative() gleichzeitig. Code wird abwechselnd in main() und cooperative() ausgeführt. Die Ausführung der Anweisungen in den Funktionen findet zwar nacheinander statt. Dank Coroutines muss aber nicht mehr eine Funktion beendet werden, bevor eine andere ausgeführt werden kann.

Beispiel 51.2. Einen Wert von einer Coroutine zurückgeben
#include <boost/coroutine/all.hpp>
#include <functional>
#include <iostream>

using boost::coroutines::coroutine;

void cooperative(coroutine<int>::push_type &sink, int i)
{
  int j = i;
  sink(++j);
  sink(++j);
  std::cout << "end\n";
}

int main()
{
  using std::placeholders::_1;
  coroutine<int>::pull_type source{std::bind(cooperative, _1, 0)};
  std::cout << source.get() << '\n';
  source();
  std::cout << source.get() << '\n';
  source();
}

Beispiel 51.2 ähnelt dem vorherigen. Das Template boost::coroutines::coroutine ist diesmal jedoch mit dem Typ int instanziiert. Damit ist es möglich, einen int-Wert von der Coroutine an den Aufrufer zu übergeben.

Die Richtung, in der der int-Wert übergeben wird, hängt davon ab, wo pull_type und push_type verwendet werden. Im Beispiel wird ein Objekt vom Typ pull_type in main() instanziiert. cooperative() hat Zugriff auf ein Objekt vom Typ push_type. Da ausschließlich push_type verwendet werden kann, um einen Wert zu senden, und ausschließlich pull_type einen Wert empfangen kann, ist die Richtung der Datenübergabe vorgegeben.

Wenn cooperative() auf sink zugreift, muss ein int-Wert als Parameter übergeben werden. Dies ist zwingend notwendig, weil sink auf dem Typ push_type basiert, der von der Klasse boost::coroutines::coroutine angeboten wird, die mit dem Template-Parameter int instanziiert wurde. Der Wert, der sink übergeben wird, kann über source in main() erhalten werden. Dazu bietet pull_type die Methode get() an.

Anhand des obigen Beispiels sehen Sie auch, wie Sie eine Funktion als Coroutine verwenden können, die mehrere Parameter erwartet. Da cooperative() einen zusätzlichen Parameter vom Typ int hat, kann die Funktion nicht direkt an den Konstruktor von pull_type übergeben werden. Im Beispiel wird std::bind() verwendet, um die Funktion mit pull_type zu verknüpfen.

Wenn Sie das Beispiel ausführen, wird 1 und 2 gefolgt von end ausgegeben.

Beispiel 51.3. Zwei Werte an eine Coroutine übergeben
#include <boost/coroutine/all.hpp>
#include <tuple>
#include <string>
#include <iostream>

using boost::coroutines::coroutine;

void cooperative(coroutine<std::tuple<int, std::string>>::pull_type &source)
{
  auto args = source.get();
  std::cout << std::get<0>(args) << " " << std::get<1>(args) << '\n';
  source();
  args = source.get();
  std::cout << std::get<0>(args) << " " << std::get<1>(args) << '\n';
}

int main()
{
  coroutine<std::tuple<int, std::string>>::push_type sink{cooperative};
  sink(std::make_tuple(0, "aaa"));
  sink(std::make_tuple(1, "bbb"));
  std::cout << "end\n";
}

Beispiel 51.3 verwendet push_type in main() und pull_type in cooperative(): Die Datenübergabe findet vom Aufrufer zur Coroutine statt.

Anhand dieses Beispiels sehen Sie auch, wie Sie mehrere Werte übergeben. Boost.Coroutine unterstützt keine Datenübergabe von mehreren Werten, so dass Sie zum Beispiel ein Tuple verwenden müssen. Sie müssen selbst mehrere Werte in ein Tuple oder in eine andere Datenstruktur packen.

Das Beispiel gibt 0 aaa, 1 bbb und end auf die Standardausgabe aus.

Beispiel 51.4. Coroutinen und Ausnahmen
#include <boost/coroutine/all.hpp>
#include <stdexcept>
#include <iostream>

using boost::coroutines::coroutine;

void cooperative(coroutine<void>::push_type &sink)
{
  sink();
  throw std::runtime_error("error");
}

int main()
{
  coroutine<void>::pull_type source{cooperative};
  try
  {
    source();
  }
  catch (const std::runtime_error &e)
  {
    std::cerr << e.what() << '\n';
  }
}

Wenn eine Ausnahme in einer Coroutine auftritt, wird die Coroutine sofort beendet. Die Ausnahme wird zum Aufrufer der Coroutine transportiert, wo sie abgefangen werden kann. Ausnahmen verhalten sich nicht anders als bei herkömmlichen Funktionsaufrufen.

Wenn Sie Beispiel 51.4 ausführen, wird error ausgegeben.