C++ Tutorial

Coroutinen

Eine Coroutine ist eine Funktion, die in ihrem Ablauf unterbrochen (suspend) und 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 einer 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 promise_type Klasse.

Sehen wir uns eine minimale Coroutinen-Klasse an (zu finden auch im Begleitmaterial in der Offline-Version unter coro_01):

1: #include <iostream>
2: #include <coroutine>
3:
4: // Die Coroutinen-Klasse
5: struct MyCoroutine
6: {
7:    struct promise_type; // Vorwaertsdeklaration
8:    using handle = std::coroutine_handle<promise_type>;
9:    // promise_type Klasse
10:   struct promise_type
11:   {
12:      // Aufruf unmittelbar nach Start der Coroutine
13:      auto initial_suspend()
14:      { return std::suspend_always{}; }
15:
16:      // Aufruf nach Beenden der Coroutine
17:      auto final_suspend() noexcept
18:      { return std::suspend_always{}; }
19:
20:      // Aufruf beim Ausloesen einer nicht behandelten
21:      // Ausnahme
22:      void unhandled_exception()
23:     { std::terminate(); }
24:
25:      // Liefert das Coroutinen-Objekt zurueck
26:      auto get_return_object()
27:     { return MyCoroutine{handle::from_promise(*this)}; }
28:
29:      // Wird durch co_return aufgerufen
30:     void return_void()
31:     { }
32:   };
33:
34:   // Setzt Coroutine fort (coro.resume()),
35:   // wenn das Coroutinen-Handle gueltig ist (ungleich 0)
36:   bool resume()
37:   { return coro ? (coro.resume(), !coro.done()) : false; };
38:
39: private:
40:   // Coroutinen-Handle
41:   handle coro;
42:   // ctor, speichert das uebergebene Coroutinen-Handle ab
43:   MyCoroutine(handle h) : coro(h) {}
44: };

Die Zeilen 10...32 definieren die eingeschlossene Klasse promise_type und die Zeilen 34...43 die Member der Coroutinen-Klasse.

Die Methode initial_suspend() (Zeile 13) 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. Liefert die Coroutine beim Beenden ein Datum zurück, 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 36) 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:

1: // Die Coroutine
2: // Rueckgabetyp ist immer die Coroutinen-Klasse
3: // Ausser der Ausgabe von Text passiert hier nichts
4: MyCoroutine CoTask()
5: {
6:    std::cout << "+++Start CoTask()\n";
7:    std::cout << "---Ende CoTask()\n";
8:    // Coroutine beenden, ohne einen Rueckgabewert
9:    // co_return definiert die Funktion fuer den Compiler
10:   // als eine Coroutine!!
11:   co_return;
12: }

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), ist sie mit der Methode resume() des Coroutinen-Objekts fortzusetzen. Und das war's auch schon.

1: int main()
2: {
3:    std::cout << "Erstelle Coroutine\n";
4:    // Coroutinen-Objekt erstellen
5:    // coro1 ist das Ergebnis des Aufrufs von
6:    // get_return_object()
7:    auto coro1 = CoTask();
8:    // Coroutine fortsetzen, da in initial_suspend()
9:    // die Coroutine unterbrochen wurde
10:   std::cout << "resume()\n";
11:   coro1.resume();
12:   std::cout << "Ende main()\n";
13: }

Coroutinen-Parameter

Die Übergabe von Daten an eine Coroutine erfolgt gleich wie bei einer Funktion.

1: // Die Coroutine
2: MyCoroutine CoTask(char toFind)
3: {...}
4: int main()
5: {
6:    // Coroutinen-Objekt erstellen
7:    auto coro1 = CoTask('E');
8:    // Coroutine fortsetzen
9:    coro1.resume();
10: }

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 zurückgegebene Datum. Damit dieses Datum später ausgelesen werden kann, muss return_value() das Datum in der promise_type Klasse zwischenspeichern.

Um auf das gespeicherte Datum zuzugreifen, ist innerhalb der Coroutinen-Klasse eine beliebige Methode zu definieren, die das Datum zurückliefert.

1: // Die Coroutinen-Klasse
2: struct MyCoroutine
3: {
4:    struct promise_type;
5:    using handle = std::coroutine_handle<promise_type>;
6:    // promise_type Klasse
7:    struct promise_type
8:    {
9:       // Letzter aus der Coroutine zurueckgegebener Wert
10:      int saved_value;
11:      ...
12:      // Aufruf durch co_return
13:      void return_value(int value)
14:      {
15:         saved_value = value;
16:      }
17:      auto final_suspend()noexcept
18:      {
19:         return std::suspend_always{};
20:      }
21:   }; // Ende von promise_type
22:   // getResult() liefert das in return_value()
23:   // abgespeicherte Datum zurueck
24:   int getResult()
25:   {
26:      return coro.promise().saved_value;
27:   }
28:
29:   ...
30: };
31: int main()
32: {
33:    auto coro1 = CoTask(search);
34:    coro1.resume();
35:    // Ergebnis der Couroutine ausgeben
36:    std::cout << coro1.getResult();
37: }

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 eine unterbrochene Coroutine fortzusetzen, wird die Methode resume() der Coroutinen-Klasse aufgerufen.

1: // Hier ist noch die Coroutinen-Klasse
2: // wie gehabt einzufuegen
3: struct MyCoroutine
4: {
5:    ...
6: };
7: // Die Coroutine
8: // Gibt die Zahlen der Fibonacci-Reihe aus
9: MyCoroutine CoTask()
10: {
11:   long val1=1, val2=1;
12:   for(;;)
13:   {
14:      // Zahl ausgeben
15:      std::println("{}",val2);
16:      // Coroutine unterbrechen
17:      co_await std::suspend_always{};
18:      // Naechste Zahl berechnen
19:      auto temp = val1;
20:      val1 = val2;
21:     val2 += temp;
22:   }
23:   co_return;
24: }
25: int main()
26: {
27:    // Coroutinen-Objekt erstellen
28:    auto coro1 = CoTask();
29:    // Fibonacci-Reihe berechnen
30:    char again;
31:    for(;;)
32:    {
33:       // Coroutine fortsetzen
34:       coro1.resume();
35:       // Hier geht's weiter wenn die Coroutine
36:       // unterbrochen ist
37:       // Abfrage, ob Coroutine fortgesetzt werden soll
38:       std::cout << "\nCoroutine fortsetzen (j/n)? ";
39:       std::cin >> again;
40:       if (again != 'j')
41:          break;
42:    }
43: }

Der Ablauf der Anwendung ist wie folgt: In Zeile 28 wird die Coroutine aufgesetzt und aufgrund der promise_type Methode initial_suspend() sofort unterbrochen. Mit dem Aufruf der resume() Methode in Zeile 34 wird die main() Funktion verlassen und die Coroutine fortgesetzt. Die Coroutine berechnet die nächste Fibonacci-Zahl und gibt diese aus. Anschließend wird sie in Zeile 17 unterbrochen und die main() Funktion in Zeile 38 fortgesetzt. Je nach Eingabe wird entweder die Coroutine erneut in Zeile 34 fortgesetzt oder die Anwendung beendet.

co_yield

Außer mit co_await kann eine Coroutine mit dem Operator

co_yield DATUM;

unterbrochen werden und das Datum DATUM an den Aufrufer zurückliefern. Nehmen wir an, die Coroutine für die Fibonacci-Reihe soll die Zahlen nur berechnen und an den Aufrufer zurückgeben.

1: // Die Coroutine
2: // Fibonacci-Reihe
3: MyCoroutine CoTask()
4: {
5:    long val1=1, val2=1;
6:    for(;;)
7:    {
8:       // Coroutine unterbrechen und Zahl zurückgeben
9:       co_yield val2;
10:      // Naechste Zahl berechnen
11:      auto temp = val1;
12:      val1 = val2;
13:      val2 += temp;
14:   }
15:   co_return;
16: }

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, wird eine weitere Methode der Coroutinen-Klasse definiert; in unserem Beispiel die Methode getValue();

1: // Die Coroutinen-Klasse
2: struct MyCoroutine
3: {
4:    struct promise_type;
5:    using handle = std::coroutine_handle<promise_type>;
6:    // promise_type Klasse
7:    struct promise_type
8:    {
9:       ...
10:      // Mittels co_yield zurueckgegebner Wert
11:      int actValue;
12:      ...
13:      // Aufruf durch co_yield
14:      auto yield_value(int value)
15:      {
16:         actValue = value;
17:        return std::suspend_always{};
18:      }
19:   }; // Ende promise_type
20:   // Auslesen des co_yield Datums
21:   int getValue()
22:   {
23:      return coro.promise().actValue;
24:   }
25:   ...
26: };

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.

Damit ist der zweite Teil, die Objektorientierung unter C++, abgeschlossen. Der dritte Teil, die C++-Standardbibliothek, ist vorläufig nur in der Offline-Version des Tutorials enthalten, aber Sie sollten nun die Basis haben, um eigene Anwendungen unter C++ erstellen zu können.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz