Koroutinen

Mit C++20 werden Koroutinen (#include <coroutine>) in den Sprachstandard aufgenommen. Mich wundert aber, dass im nächsten C Standard (C2x) von Koroutinen keine Rede ist.

Also habe ich mich mal in den Betriebssystemen umgesehen, wie man Koroutinen selbst implementieren kann.


Vorgeschichte

Definition: Ich verstehe unter Koroutinen die Möglichkeit, einen “synchronen” Ausführungskontext anzuhalten und auf einen anderen zu wechseln. Eigentlich meine ich damit eine Form des User-Mode-Thread-Scheduling.
Diverse “akademische” Spielereien, wo man selbst eine State-Machine implementiert und eine Funktion manuell in Stücke zerlegt und diese aufruft, fällt für mich nicht unter das Sprachenkonstrukt “Koroutinen”, wenn gleich man funktional damit das Gleiche machen kann.

Ich möchte also mehrere Ausführungskontexte in einer logischen Einheit

  • starten (create)
  • gezielt aktivieren (switch_to)
  • zufällig aktivieren (yield)
  • abwarten können (wait)

Persönlich gab es für mich noch nie einen Grund, bewusst Koroutinen einzusetzen, sie sind für mich eher nur Syntax-Zucker.
Ich gebe aber zu, dass es Konstellationen gibt (z.B. in Callback-Funktionen), wo es hilfreich sein kann, einfach auf einen anderen Callstack zu wechseln um dort etwas abzuarbeiten, ohne dass man gleich Kernel-Threads und Messagequeues dafür einsetzt.

Win32 Fiber API

Windows kennt seit Win32/WinNT “Fibers” (Fasern) als Untermenge von Threads (Faden). Ein Thread führt in der Regel genau einen Callstack mit sich, arbeitet also “eine Faser (=Fiber)” ab. Man kann aber einen Thread in eine Fiber umwandeln und weitere Fibers erzeugen und dann per API zwischen diesen “Fasern” hin und herwechseln. Es läuft immer nur ein aktiver Fiber in dem Thread, und alle anderen sind angehalten (werden also nicht ausgeführt).

Mit dieser Technik kann man Koroutinen relativ einfach implementieren, in dem man eine Scheduler-Fiber-Funktion nutzt, die sequentiell andere registrierte Fiber-Koroutinen-Kontexte aktiviert. Wenn eine Fiber-Koroutine zu einer anderen wechseln möchte, teilt sie das über eine gemeinsame Variable mit und wechselt dann zur Scheduler-Fiber, die die Anweisung korrekt umsetzt.

Und wenn jetzt zu jeder Fiber noch ein completed Flag gespeichert wird, das beim Verlassen einer Koroutinen-Funktion gesetzt wird, dann kann man auch ein wait Feature implementieren, womit dann eine wartende Koroutine so lange nicht weiterläuft, bis die “erwartete” andere Routine fertig ist.

Folgende Windows API sind sehr hilfreich:

Eigentlich gibt es dann noch die Funktion ConvertFiberToThread mit welcher man einen Fiber-Thread wieder zu einem normalen Thread machen kann. Da diese Funktion aber erst mit Server 2003 eingeführt wurde und ich Koroutinen sowieso Thread-technisch abkapseln möchte, starte ich einen eigenen Thread für eine Koroutinen-Session. Wird dieser beendet, sterben seine “Fasern” mit ihm und ich muss mir keine Gedanken über COM-Aufrufe oder Ähnliches machen.

Linux/POSIX ucontext

POSIX hatte mit dem ucontext.h Header ebenfall ein Werkzeug bereitgestellt, mit dem man selbst einen Stack-Block allokieren und damit einen neuen Aurufkontext erstellen und dann darauf hinwechseln kann.

Auch hier kann man ganz analog zu Windows Fibers mehrere Aufrufkontexte als Teil eines Threads erstellen und dann manuell zwischen diesen hin- und herwechseln.

Die notwendigen APIs lauten:

Interessant ist, dass man den zu nutzenden Stack-Speicher mit malloc selbst bereitstellen kann, es wäre aber auch erlaubt, einfach ein paar Kilobytes des eigenen Stack-Frames für die Koroutine einzusetzen, in dem man einfach ein char coroutine_stack[16384] in die eigene Routine schreibt.
Aber um Stapelüberläufe zu verhindern, sollte man eher den ersten Weg gehen.

Einen fetten Nachteil gibt es leider: ucontext wurde nachträglich wieder aus dem POSIX Standard entfernt. Linux hat dieses Feature großteils beibehalten, aber System wie OpenBSD haben es aus seiner Implementierung gestrichen.

Mein vorläufig einzige Möglichkeit für Koroutinen auf solchen Systemen ist der Einsatz von “echten” Threads für jede Koroutine, die einfach schlafen gelegt werden, wenn sie einen Kontext-Wechsel einfordern.

Fazit

Am Ende verstehe ich das Problem, warum C++20 hier sein ganz eigenes Koroutinen-Konzept in die Sprache einbaut und nicht auf Bibliotheken zurückgreift:

Andere Koroutinen-Implmentierungen (wie lib coro) brauchen am Ende Assembler-Blöcke für jeden möglichen CPU-Typ, um Koroutinen auf jede Plattform bringen zu können, weil das OS diesen Support nicht immer bietet und auch Mikrokontroller sind davon ausgeschlossen.

Man muss dieses Feature also in Core-Language aufnehmen und da gegen solche Änderungen sträubt sich C, weil es sein Einfachheit verlieren könnte.

C++ leistet sich diesen Luxus hingegen und wird (so meine Erwartung) vermutlich damit den bestmöglichen Koroutinen-Support anbieten können, den man sich vorstellen kann.

Ein Problem wird es aber meines Erachtens immer geben: Bestehende Bibliotheken oder auch Betriebssysteme kennen das C++20 Koroutinen Modell nicht und wenn man diese mit nativen Funktionen mischt, könnten mehrere Seiteneffekt auftreten.

Man denke an native Warte-Routinen oder andere eigene oder systemspezifische Implementierungen. Von daher würde ich Koroutinen in der Systemprogrammierung auch weiter nicht einsetzen … und das ist Schade. Beschränkt man sich aber auf Mathematik und nur auf std Implementierung, haben Koroutinen sicher ein gutes Potential.

Und ich kann mit meinen Implementierung in C zumindest ein bisschen auf dieser Welle mitreiten, wohlwissend, dass diese Variante noch anfälliger für Seiteneffekt ist.


Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!