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