Coroutinen
Definition
Eine Coroutine ist eine Funktion, die in ihrem Ablauf unterbrochen (suspend) und später an der Stelle fortgesetzt (resume) werden kann, an der sie unterbrochen wurde. Der Status der Funktion bleibt dabei erhalten, d.h., ihre Daten werden sozusagen bei der Unterbrechung eingefroren. Die Unterbrechung der Coroutine kann nur durch die Coroutine selbst erfolgen.
Eine Coroutine wird dadurch definiert, dass innerhalb der Funktion mindestens einer der unären Operatoren co_await, co_yield oder co_return verwendet wird. Erkennt der Compiler einen der Operatoren in einer Funktion, erzeugt er automatisch den für eine Coroutine erforderlichen Rahmen.
Für Coroutinen gibt es eine Reihe von Einschränkungen. So dürfen sie keine variadischen oder auto Parameter besitzen und die Rückkehr aus einer Coroutine darf nur mit co_return erfolgen.
Coroutinen-Klasse und promise_type
Die 'Kommunikation' mit einer Coroutine erfolgt intern über ein Handle (eindeutiger Bezeichner) vom Typ promise_type. Die zu definierende Klasse promise_type steuert den Ablauf der Coroutine. Dazu muss promise_type mindestens die 5 Methoden initial_suspend(), final_suspend(), return_void(), unhandled_exception() und get_return_object() definieren.
Die definierte promise_type Klasse ist in einer weiteren Klasse (im Folgenden als Coroutinen-Klasse bezeichnet) einzuschließen. Die Coroutinen-Klasse besteht im einfachsten Fall nur aus einem Konstruktor, der das Handle abspeichert, einer Methode resume(), um eine unterbrochene Coroutine fortzusetzen, und der erwähnten eingeschlossenen promise_type Klasse.
Sehen wir uns eine minimale Coroutinen-Klasse an:
// Datei coro_01.h
// Definition der Coroutinen-Klasse
#ifndef coro_01_h
#define coro_01_h
#include <coroutine>
// Die Coroutinen-Klasse
struct MyCoroutine
{
struct promise_type; // Vorwaertsdeklaration
using handle = std::coroutine_handle<promise_type>;
// promise_type Klasse
struct promise_type
{
// Aufruf unmittelbar nach Start der Coroutine
auto initial_suspend()
{ return std::suspend_always{}; }
// Aufruf nach Beenden der Coroutine
auto final_suspend() noexcept
{ return std::suspend_always{}; }
// Aufruf beim Ausloesen einer nicht behandelten
// Ausnahme
void unhandled_exception()
{ std::terminate(); }
// Liefert das Coroutinen-Objekt zurueck
auto get_return_object()
{ return MyCoroutine{handle::from_promise(*this)}; }
// Wird durch co_return aufgerufen
void return_void()
{ }
};
// Setzt Coroutine fort (coro.resume()),
// wenn das Coroutinen-Handle gueltig ist (ungleich 0)
bool resume()
{ return coro ? (coro.resume(), !coro.done()) : false; };
private:
// Coroutinen-Handle
handle coro;
// ctor, speichert das uebergebene Coroutinen-Handle ab
MyCoroutine(handle h) : coro(h) {}
};
#endif
Die Zeilen 15...37 definieren die eingeschlossene Klasse promise_type und die Zeilen 39...48 die Member der Coroutinen-Klasse.
Die Methode initial_suspend() wird unmittelbar nach der Initialisierung des Coroutinen-Rahmens aufgerufen. Gibt sie ein Objekt vom Typ suspend_always zurück, wird die Coroutine sofort unterbrochen. Soll die Coroutine zu Beginn nicht unterbrochen werden, ist anstelle eines suspend_always-Objekts ein Objekt vom Typ suspend_never zurückzugeben.
final_suspend() wird beim Beenden der Coroutine ausgeführt. Hier können bei Bedarf eventl. Aufräumarbeiten durchgeführt werden. Auch diese Methode gibt ein Objekt vom Typ suspend_always bzw. suspend_never zurück.
Die nächste Methode unhandled_exception() wird aufgerufen, wenn in der Coroutine eine Ausnahme ausgelöst wird, für die kein Exception-Handler definiert ist. In der Regel wird in dieser Methode die Anwendung durch einen Aufruf von terminate() beendet.
Die Methode get_return_object() liefert das Coroutinen-Objekt zurück. Über dieses Objekt kann die Anwendung später z.B. die Coroutine fortsetzen (resume).
Und die letzte Methode return_void() der promise_type Klasse wird durch den Operator co_return aufgerufen, wenn die Coroutine kein Datum zurückliefert. Soll die Coroutine beim Beenden ein Datum zurückgeben, ist anstelle der Methode return_void() die Methode return_value() zu implementieren. Mehr dazu später.
Bleiben noch die restlichen Anweisungen in der Coroutinen-Klasse. Die Methode resume() (Zeile 41) veranlasst, dass eine unterbrochene Coroutine fortgesetzt wird. Dies ist natürlich nur möglich, wenn die fortzusetzende Coroutine zwischenzeitlich nicht beendet wurde.
Und zum Schluss legt der Konstruktor der Coroutinen-Klasse das Handle für die Coroutine in der Eigenschaft coro ab. Beachten Sie, dass der Konstruktor in einer private-Sektion liegt und damit nicht so ohne Weiteres ein Objekt vom Typ der Coroutinen-Klasse definiert werden kann.
Damit haben wir alle erforderlichen Schnittstellen und Eigenschaften für unsere erste Coroutine definiert. Wenn Sie später eigene Coroutinen erstellen, können Sie diese Coroutinen-Klasse als Vorlage verwenden.
Definition einer Coroutine
Eine Coroutine wird prinzipiell genauso definiert, wie eine 'normale' Funktion. Der Returntyp der Coroutine ist immer vom Typ der Coroutinen-Klasse. Für unsere Coroutinen-Klasse könnte eine erste Coroutine wie folgt aussehen:
#include <print>
#include "coro_01.h"
// Die Coroutine
// Rueckgabetyp ist immer die Coroutinen-Klasse
// Ausser der Ausgabe von Text passiert hier noch nichts
MyCoroutine CoTask()
{
std::println("+++Start CoTask()");
// Coroutine beenden, ohne einen Rueckgabewert
// co_return definiert die Funktion fuer den Compiler
// als eine Coroutine!!
std::println("---Ende CoTask()");
co_return;
}
int main()
{ }
Außer der Ausgabe eines Textes führt die Coroutine nichts aus. Beachten Sie, dass am Ende der Coroutine ein co_return steht und kein return!
Ausführen einer Coroutine
Um die Coroutine zu starten, wird diese aufgerufen und das zurückgegebene Coroutinen-Objekt abgespeichert. Da unsere Coroutine beim Start unterbrochen wurde (initial_suspend()-Methode), muss sie mit der Methode resume() des Coroutinen-Objekts fortgesetzt werden, damit die Ausgaben erfolgen.
Und das war's auch schon. Nachfolgend der komplette Code der ersten Coroutine.
#include <print>
#include "coro_01.h"
// Die Coroutine
// Rueckgabetyp ist immer die Coroutinen-Klasse
// Ausser der Ausgabe von Text passiert hier nichts
MyCoroutine CoTask()
{
std::println("+++Start CoTask()");
// Coroutine beenden, ohne einen Rueckgabewert
// co_return definiert die Funktion fuer den Compiler
// als eine Coroutine!!
std::println("---Ende CoTask()");
co_return;
}
int main()
{
std::println("Erstelle Coroutine");
// Coroutinen-Objekt erstellen
// coro1 ist das Ergebnis des Aufrufs von get_return_object()
auto coro1 = CoTask();
// Coroutine fortsetzen, da in initial_suspend()
// die Coroutine unterbrochen wurde
std::println("resume()");
coro1.resume();
std::println("Ende main()");
}
Erstelle Coroutine
resume()
+++Start CoTask()
---Ende CoTask()
Ende main()
Übung
coro_02:
Die Datei swr1_hitparade.csv enthält (fast) alle Musiktitel ab der Position 1000, die in der SWR1-Hitparade seit 1989 vertreten waren. Die Datei hat folgenden prinzipiellen Aufbau:
Titel;Interpret;1989;1990;...;2025
dreadlock holiday;10cc;419;896;...;0
eagle;abba;0;0;...;436
...
Die erste Zeile ist die Titelzeile mit den Spaltenbeschriftungen, d. h., die erste Spalte enthält den Titel, die zweite den Interpreten und anschließend folgen 25 Spalten mit den Jahreszahlen (einige Jahre wurden übersprungen).
Danach folgen die Zeilen mit den entsprechenden Hitparadeneinträgen. Beachten Sie, dass jeweils der letzte Wert in einer Zeile nicht mit einem Semikolon abgeschlossen ist.
Geben Sie in einer Coroutine die ersten 10 Einträge aus, deren Interpret mit einem 'a' beginnt. Geben Sie die Zeilennummer, den Interpret und den Titel aus.
Hinweis: Da die Coroutine prinzipiell eine Funktion ist, können an sie auch Parameter übergeben werden.
Zeile: 20, Interpr.: annenmaykantereit, Titel: 3 tage am meer
Zeile: 33, Interpr.: alice, Titel: a cosa pensano
Zeile: 74, Interpr.: adiemus, Titel: adiemus
Zeile: 86, Interpr.: ac/dc, Titel: ain't no fun
Zeile: 110, Interpr.: ace of base, Titel: all that she wants
Zeile: 126, Interpr.: a fine frenzy, Titel: almost lover
Zeile: 144, Interpr.: andreas gabalier, Titel: amoi seg' ma uns wieder
Zeile: 175, Interpr.: ac/dc, Titel: anything she goes
Zeile: 192, Interpr.: andrea berg, Titel: atlantis lebt
Zeile: 195, Interpr.: andreas bourani, Titel: auf anderen wegen
Rückgabewert einer Coroutine
Genauso wie eine Funktion kann eine Coroutine ein Datum zurückliefern. Dies erfolgt durch Angabe des Datums nach dem Operator co_return:
co_return DATUM;
Da nun co_return ein Datum zurückliefert, ist die promise_type Methode return_void() durch return_value() zu ersetzen. Sie erhält als einzigen Parameter das von der Coroutine mittels co_return zurückgegebene Datum. Damit dieses Datum später ausgelesen werden kann, muss das Datum in der promise_type Klasse zwischenspeichern.
Es darf entweder die Methode return_void() oder return_value() definiert sein, ansonsten gibt es beim Übersetzen einen Fehler!
Damit das zwischengespeicherte Datum nach Verlassen der Coroutine noch gültig ist, darf die Methode final_suspend() kein Objekt vom Typ suspend_never zurückgeben.
Um auf das gespeicherte Datum zuzugreifen, ist innerhalb der Coroutinen-Klasse eine beliebige Methode zu definieren, die das Datum zurückliefert.
// Datei coro_02-h
// Definition der Coroutinen-Klasse
#ifndef coro_02_h
#define coro_02_h
#include <coroutine>
// Die Coroutinen-Klasse
template <typename T>
struct MyCoroutine
{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
// promise_type Klasse
struct promise_type
{
// Letzter aus der Coroutine zurueckgegebener Wert
T saved_value;
// Aufruf unmittelbar nach Start der Coroutine
auto initial_suspend()
{
return std::suspend_always{};
}
// Aufruf nach Beenden der Coroutine
auto final_suspend() noexcept
{
return std::suspend_always{};
}
// Aufruf beim Ausloesen einer nicht behandelten
// Ausnahme
void unhandled_exception()
{
std::terminate();
}
// Liefert das Coroutinen-Objekt zurueck
auto get_return_object()
{
return MyCoroutine{ handle::from_promise(*this) };
}
// Aufruf durch co_return
void return_value(T value)
{
saved_value = value;
}
};
// Setzt die Ausfuehrung der Coroutine fort (coro.resume()),
// wenn das Coroutinen-Handle gueltig ist (ungleich 0)
bool resume()
{
return coro ? (coro.resume(), !coro.done()) : false;
}
// const-Referenz auf Datum zurueckgeben
const T& GetValue() const
{
return coro.promise().saved_value;
}
private:
// Coroutinen-Handle
handle coro;
// ctor, speichert das uebergebene Coroutinen-Handle ab
MyCoroutine(handle h) : coro(h)
{
}
};
#endif
Die Definition der Klasse MyCoroutine als Klassentemplate ist nicht zwingend erforderlich. Sie ist hier nur aufgeführt um gleich aufzuzeigen, wie Coroutinen, die mit unterschiedlichen Datentypen arbeiten, definiert werden können.
Ein einfach Anwendung könnte dann so aussehen:
#include <print>
#include "coro_02.h"
// Die Coroutine 1, liefert int-Datum zurueck
MyCoroutine<int> CoTask1()
{
std::println("+++Start CoTask1()");
// Coroutine beenden, ohne einen Rueckgabewert
// co_return definiert die Funktion fuer den Compiler
// als eine Coroutine!!
std::println("---Ende CoTask1()");
co_return 10;
}
// Eine zweite Coroutine, liefert float-Datum zurueuck
MyCoroutine<float> CoTask2()
{
std::println("+++Start CoTask2()");
// Coroutine beenden, ohne einen Rueckgabewert
// co_return definiert die Funktion fuer den Compiler
// als eine Coroutine!!
std::println("---Ende CoTask2()");
co_return 3.14f;
}
int main()
{
auto coro1 = CoTask1();
coro1.resume();
std::println("CoTask1 liefert {}",coro1.GetValue());
auto coro2 = CoTask2();
coro2.resume();
std::println("CoTask2 liefert {}",coro2.GetValue());
}
+++Start CoTask1()
---Ende CoTask1()
CoTask1 liefert 10
+++Start CoTask2()
---Ende CoTask2()
CoTask2 liefert 3.14
Übung
coro_03:
Erweitern Sie die vorherige Übung coro_02 so, dass alle Einträge gesucht und ausgegeben werden bei denen der Interpret mit dem Buchstaben 'a' beginnt.
Geben Sie aus der Coroutine die Anzahl der gefundenen Einträge zurück und geben diese in main() aus.
...
Zeile: 4153, Interpr.: avicii, Titel: levels
Zeile: 4154, Interpr.: abba, Titel: my love, my life
Zeile: 4160, Interpr.: alphaville, Titel: sounds like a melody
249 Interpreten mit a gefunden.
Unterbrechen einer Coroutine
co_wait
Alles, was unsere Coroutine bisher durchgeführt hat, ließe sich auch durch eine 'normale' Funktion erledigen. Sinn und Zweck einer Coroutine ist aber, dass sich deren Ablauf unterbrechen und fortsetzen lässt, ohne dass dabei deren aktueller Zustand verloren geht.
Um eine Coroutine an einer beliebigen Stelle zu unterbrechen wird der Operator
co_await AWAITER;
verwendet. AWAITER ist ein Objekt, welches die Bedingung für eine Unterbrechung liefert. Soll die Coroutine auf jeden Fall unterbrochen werden, ist für AWAITER std::suspend_always{} einzusetzen.
Um die unterbrochene Coroutine fortzusetzen, wird die Methode resume() der Coroutinen-Klasse aufgerufen.
#include <print>
#include <iostream>
#include "coro_01.h"
// Die Coroutine
// Gibt fortlaufende Werte aus und pausiert dann
MyCoroutine CoTask()
{
int counter = 0;
std::println("+++Start CoTask()");
// Werte ausgeben
for (;;)
{
counter++;
std::print("{},",counter);
// Nach 10 Werten pausieren
if ((counter%10) == 0)
{
std::println("");
co_await std::suspend_always{};
}
}
std::println("---Ende CoTask()");
co_return;
}
int main()
{
std::println("Erstelle Coroutine");
// Coroutinen-Objekt erstellen
auto coro1 = CoTask();
char again;
// Endlosschleife
for(;;)
{
coro1.resume();
std::print("Noch eine Reihe? ");
std::cin >> again;
if (again != 'j')
break;
}
std::println("Ende main()");
}
Erstelle Coroutine
+++Start CoTask()
1,2,3,4,5,6,7,8,9,10,
Noch eine Reihe? j
11,12,13,14,15,16,17,18,19,20,
Noch eine Reihe? j
21,22,23,24,25,26,27,28,29,30,
Noch eine Reihe? n
Ende main()
Der Ablauf der Anwendung ist wie folgt: In Zeile 31 wird die Coroutine aufgesetzt und aufgrund der promise_type Methode initial_suspend() sofort unterbrochen. Mit dem Aufruf der resume() Methode in Zeile 36 wird die main() Funktion verlassen und die Coroutine fortgesetzt. Die Coroutine gibt die nächsten 10 Werte aus. Anschließend wird sie in Zeile 20 unterbrochen und die main() Funktion in Zeile 37 fortgesetzt. Je nach Eingabe wird entweder die Coroutine erneut in Zeile 36 fortgesetzt oder die Anwendung beendet.
co_yield
Außer mit co_await kann eine Coroutine mit dem Operator
co_yield DATUM;
unterbrochen werden und dabei das Datum DATUM an den Aufrufer zurückliefern. Nehmen wir an, die Coroutine fürt die Ausgabe der Zahlen soll die letzte ausgegebene Zahl an den Aufrufer zurückgeben.
// Die Coroutine
// Gibt fortlaufende Werte aus und pausiert dann
MyCoroutine CoTask()
{
...
if ((counter%10) == 0)
{
std::println("");
co_yeald counter;
}
...
}
Auch für diesen Fall ist die promise_type Klasse etwas zu erweitern. Genauso wie co_return DATUM; eine zusätzliche Methode return_value() erfordert, erfordert co_yield DATUM; eine Methode yield_value(), die als Parameter das zurückzugebende Datum erhält und dieses intern zwischenspeichern muss.
Um das von co_yield abgespeicherte Datum auszulesen, muss noch eine weitere Methode der Coroutinen-Klasse definiert werden.
Erweitern wir das vorherige Beispiel um die Ausgabe des aktuellen Zählerstandes beim Pausieren der Coroutine.
// Datei coro_04.h
#ifndef coro_04_h
#define coro_04_h
#include <coroutine>
// Die Coroutinen-Klasse
template <typename T = int>
struct MyCoroutine
{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
// promise_type Klasse
struct promise_type
{
// Letzter aus der Coroutine zurueckgegebener Wert
T saved_value;
// Mit co_yield zurueckgegebener Wert
T act_value;
// Aufruf unmittelbar nach Start der Coroutine
auto initial_suspend()
{
return std::suspend_always{};
}
// Aufruf nach Beenden der Coroutine
auto final_suspend() noexcept
{
return std::suspend_always{};
}
// Aufruf beim Ausloesen einer nicht behandelten
// Ausnahme
void unhandled_exception()
{
std::terminate();
}
// Liefert das Coroutinen-Objekt zurueck
auto get_return_object()
{
return MyCoroutine{ handle::from_promise(*this) };
}
// Aufruf durch co_return;
void return_void()
{ }
// Aufruf durch co_yield DATUM;
auto yield_value(T value)
{
act_value = value;
return std::suspend_always{};
}
};
// Setzt die Ausfuehrung der Coroutine fort (coro.resume()),
// wenn das Coroutinen-Handle gueltig ist (ungleich 0)
bool resume()
{
return coro ? (coro.resume(), !coro.done()) : false;
}
// const-Referenz auf das co_return Datum zurueckgeben
const T& GetValue() const
{
return coro.promise().saved_value;
}
// const-Referenz auf das co_yield Datum zurueckgeben
const T& GetActValue() const
{
return coro.promise().act_value;
}
private:
// Coroutinen-Handle
handle coro;
// ctor, speichert das uebergebene Coroutinen-Handle ab
MyCoroutine(handle h) : coro(h)
{
}
};
#endif
#include <print>
#include <iostream>
#include "coro_04.h"
// Die Coroutine
// Gibt 10 fortlaufende Werte aus und pausiert dann
MyCoroutine<int> CoTask()
{
int counter = 0;
std::println("+++Start CoTask()");
// Werte ausgeben
for (;;)
{
counter++;
std::print("{},", counter);
// Nach 10 Werten pausieren
if ((counter % 10) == 0)
{
std::println("");
// Akt. Zaehler zurueckgeben
co_yield counter;
}
}
std::println("---Ende CoTask()");
co_return;
}
int main()
{
std::println("Erstelle Coroutine");
// Coroutinen-Objekt erstellen
auto coro1 = CoTask();
char again;
// Endlosschleife
for (;;)
{
coro1.resume();
// Akt. Zaehler auslesen und ausgeben
auto last_value = coro1.GetActValue();
std::print("Letzter Wert war: {}\nNoch eine Reihe? ",last_value);
std::cin >> again;
if (again != 'j')
break;
}
std::println("Ende main()");
}
Erstelle Coroutine
+++Start CoTask()
1,2,3,4,5,6,7,8,9,10,
Letzter Wert war: 10
Noch eine Reihe? j
11,12,13,14,15,16,17,18,19,20,
Letzter Wert war: 20
Noch eine Reihe? n
Ende main()
Beenden wir damit die Einführung von Coroutinen. Wir haben uns in diesem Kapitel nur die Grundlagen zu Coroutinen angesehen, aber Sie sollten jetzt eine ungefähre Vorstellung haben, wie Coroutinen arbeiten.
Übung
coro_04:
Schreiben Sie die Coroutine aus der Übung coro_03 so um, dass die Coroutine nicht mehr die gefunden Titel ausgibt sondern diese in main() ausgegeben werden.
Nach jedem 5. Titel ist abzufragen, ob weitere Titel eingelesen werden sollen.
Zeile: 30, Interpr.: annenmaykantereit, Titel: 3 tage am meer
Zeile: 52, Interpr.: alice, Titel: a cosa pensano
Zeile: 64, Interpr.: art garfunkel, Titel: a heart in new york
Zeile: 65, Interpr.: america, Titel: a horse with no name
Zeile: 110, Interpr.: adiemus, Titel: adiemus
Weitere Daten einlesen (j/n)? j
Zeile: 131, Interpr.: ac/dc, Titel: ain't no fun (waiting round to be a millionaire)
Zeile: 171, Interpr.: ace of base, Titel: all that she wants
Zeile: 193, Interpr.: a fine frenzy, Titel: almost lover
Zeile: 194, Interpr.: achim reichel, Titel: aloha heja he
Zeile: 216, Interpr.: aerosmith, Titel: amazing
Weitere Daten einlesen (j/n)? n
Damit ist der Teil Objektorientierte Programmierung abgeschlossen. Im nächsten Teil C++-Standardbibliothekgeht es um Anwendung des erworbenen Wissens mit Hilfe der in der Standardbibliothek definierten Datentypen und Funktionen.