Coroutine

Seit 2003 bieten viele der populärsten Programmiersprachen, darunter C und seine Derivate, keine direkte Unterstützung für Coroutines in der Sprache oder ihren Standardbibliotheken. Dies ist größtenteils auf die Beschränkungen der Stack-basierten Implementierung von Unterprogrammen zurückzuführen. Eine Ausnahme ist die C++-Bibliothek Boost.Context, Teil der Boost-Bibliotheken, die Context Swapping auf ARM, MIPS, PowerPC, SPARC und x86 unter POSIX, Mac OS X und Windows unterstützt. Coroutines können auf Boost.Context aufgebaut werden.

In Situationen, in denen eine Coroutine die natürliche Implementierung eines Mechanismus wäre, aber nicht zur Verfügung steht, besteht die typische Reaktion darin, eine Closure zu verwenden – eine Subroutine mit Zustandsvariablen (statische Variablen, oft boolesche Flags), um einen internen Zustand zwischen Aufrufen aufrechtzuerhalten und die Kontrolle an den richtigen Punkt zu übertragen. Konditionale Bedingungen innerhalb des Codes führen dazu, dass bei aufeinanderfolgenden Aufrufen verschiedene Codepfade ausgeführt werden, die auf den Werten der Zustandsvariablen basieren. Eine weitere typische Reaktion ist die Implementierung eines expliziten Zustandsautomaten in Form einer großen und komplexen Switch-Anweisung oder über eine goto-Anweisung, insbesondere ein berechnetes goto. Solche Implementierungen gelten als schwierig zu verstehen und zu pflegen und sind ein Grund für die Unterstützung von Coroutines.

Threads und in geringerem Maße Fasern sind heute in den gängigen Programmierumgebungen eine Alternative zu Coroutines. Threads bieten Möglichkeiten zur Verwaltung der kooperativen Echtzeit-Interaktion von gleichzeitig ausgeführten Codeteilen. Threads sind in Umgebungen, die C unterstützen, weit verbreitet (und werden in vielen anderen modernen Sprachen nativ unterstützt), sind vielen Programmierern vertraut und sind in der Regel gut implementiert, gut dokumentiert und werden gut unterstützt. Da sie jedoch ein großes und schwieriges Problem lösen, umfassen sie viele leistungsstarke und komplexe Funktionen und haben eine entsprechend schwierige Lernkurve. Wenn eine Coroutine alles ist, was benötigt wird, kann die Verwendung eines Threads ein Overkill sein.

Ein wichtiger Unterschied zwischen Threads und Coroutines ist, dass Threads in der Regel präemptiv geplant werden, während Coroutines dies nicht tun. Da Threads jederzeit neu geplant werden können und gleichzeitig ausgeführt werden können, müssen Programme, die Threads verwenden, vorsichtig mit dem Sperren sein. Da Coroutines nur zu bestimmten Zeitpunkten im Programm neu geplant werden können und nicht gleichzeitig ausgeführt werden, können Programme, die Coroutines verwenden, Sperren oft ganz vermeiden. Diese Eigenschaft wird auch als Vorteil der ereignisgesteuerten oder asynchronen Programmierung angeführt.

Da Fasern kooperativ geplant werden, bieten sie eine ideale Grundlage für die Implementierung der oben genannten Coroutines. Allerdings fehlt es oft an Systemunterstützung für Fasern im Vergleich zu Threads.

Implementierungen für CEdit

Um Allzweck-Coroutinen zu implementieren, muss ein zweiter Aufrufstapel beschafft werden, eine Funktion, die von der Sprache C nicht direkt unterstützt wird. Ein zuverlässiger (wenn auch plattformspezifischer) Weg, dies zu erreichen, ist die Verwendung einer kleinen Menge von Inline-Assembly, um den Stack-Zeiger während der anfänglichen Erstellung der Coroutine explizit zu manipulieren. Dieser Ansatz wird von Tom Duff in einer Diskussion über die relativen Vorzüge gegenüber der von Protothreads verwendeten Methode empfohlen. Auf Plattformen, die den POSIX-Systemaufruf sigaltstack zur Verfügung stellen, kann ein zweiter Aufrufstapel durch den Aufruf einer Sprungbrettfunktion aus einem Signalhandler heraus erreicht werden, um das gleiche Ziel in portablem C zu erreichen, allerdings auf Kosten einer gewissen zusätzlichen Komplexität. C-Bibliotheken, die POSIX oder der Single Unix Specification (SUSv3) entsprechen, stellten Routinen wie getcontext, setcontext, makecontext und swapcontext zur Verfügung, aber diese Funktionen wurden in POSIX 1.2008 für veraltet erklärt.

Wenn ein zweiter Aufrufstapel mit einer der oben genannten Methoden erhalten wurde, können die Funktionen setjmp und longjmp in der Standard-C-Bibliothek verwendet werden, um den Wechsel zwischen Coroutines zu implementieren. Diese Funktionen speichern bzw. stellen den Stack-Zeiger, den Programmzähler, die vom Aufruf gespeicherten Register und alle anderen internen Zustände wieder her, wie sie von der ABI gefordert werden, so dass bei der Rückkehr zu einer Coroutine nach einem Yield alle Zustände wiederhergestellt werden, die bei der Rückkehr von einem Funktionsaufruf wiederhergestellt werden würden. Minimalistische Implementierungen, die die setjmp- und longjmp-Funktionen nicht huckepack nehmen, können das gleiche Ergebnis durch einen kleinen Block von Inline-Assembler erreichen, der lediglich den Stack-Zeiger und den Programmzähler austauscht und alle anderen Register blockiert. Dies kann erheblich schneller sein, da setjmp und longjmp konservativ alle Register speichern müssen, die laut ABI in Gebrauch sein könnten, während die clobber-Methode es dem Compiler erlaubt, nur das zu speichern (durch Auslagerung auf den Stack), von dem er weiß, dass es tatsächlich in Gebrauch ist.

Aufgrund der fehlenden direkten Sprachunterstützung haben viele Autoren ihre eigenen Bibliotheken für Coroutines geschrieben, die die oben genannten Details verbergen. Die libtask-Bibliothek von Russ Cox ist ein gutes Beispiel für dieses Genre. Sie verwendet die Kontextfunktionen, wenn sie von der nativen C-Bibliothek bereitgestellt werden; andernfalls bietet sie ihre eigenen Implementierungen für ARM, PowerPC, Sparc und x86. Andere bemerkenswerte Implementierungen sind libpcl, coro, lthread, libCoroutine, libconcurrency, libcoro, ribs2, libdill, libaco und libco.

Zusätzlich zum obigen allgemeinen Ansatz wurden mehrere Versuche unternommen, Coroutines in C durch Kombinationen von Unterprogrammen und Makros zu approximieren. Simon Tathams Beitrag, der auf Duffs Gerät basiert, ist ein bemerkenswertes Beispiel für dieses Genre und bildet die Grundlage für Protothreads und ähnliche Implementierungen. Zusätzlich zu Duffs Einwänden liefern Tathams eigene Kommentare eine offene Einschätzung der Grenzen dieses Ansatzes: „Soweit ich weiß, ist dies das schlechteste Stück C-Hackerei, das je in ernsthaftem Produktionscode gesehen wurde.“ Die Hauptmängel dieser Annäherung bestehen darin, dass kein separater Stack-Frame für jede Coroutine aufrechterhalten wird, dass lokale Variablen bei Ausgängen aus der Funktion nicht erhalten bleiben, dass es nicht möglich ist, mehrere Einträge in die Funktion zu haben, und dass die Kontrolle nur von der Top-Level-Routine abgegeben werden kann.

Implementierungen für C++Edit

  • C++-Coroutinen TS (Technical Specification), ein Standard für C++-Spracherweiterungen für eine stapellose Teilmenge von Coroutine-ähnlichem Verhalten, ist in der Entwicklung. Visual C++ und Clang unterstützen bereits große Teile im std::experimental-Namensraum. coroutines Technical Specification
  • Boost.Coroutine – erstellt von Oliver Kowalke, ist die offiziell freigegebene portable Coroutine-Bibliothek von boost seit Version 1.53. Die Bibliothek basiert auf Boost.Context und unterstützt ARM, MIPS, PowerPC, SPARC und X86 unter POSIX, Mac OS X und Windows.
  • Boost.Coroutine2 – ebenfalls von Oliver Kowalke erstellt, ist eine modernisierte portable Coroutine-Bibliothek seit boost Version 1.59. Sie nutzt die Vorteile von C++11-Features, entfernt aber die Unterstützung für symmetrische Coroutines.
  • Mordor – Im Jahr 2010 stellte Mozy eine C++-Bibliothek zur Verfügung, die Coroutines implementiert, wobei der Schwerpunkt auf ihrer Verwendung zur Abstraktion asynchroner E/A in ein vertrauteres sequenzielles Modell liegt.
  • CO2 – stapellose Coroutine, die auf C++-Präprozessortricks basiert und eine await/yield-Emulation bietet.
  • ScummVM – Das ScummVM-Projekt implementiert eine leichtgewichtige Version von stapellosen Coroutinen, die auf dem Artikel von Simon Tatham basiert.
  • tonbit::coroutine – C++11 single .h asymmetrische Coroutine-Implementierung über ucontext / fiber
  • Coroutines sind im Mai 2017 in Clang gelandet, die libc++-Implementierung läuft.
  • elle von Docker
  • oatpp-coroutines – stapellose Coroutines mit Scheduling, die für I/O-Operationen mit hoher Gleichzeitigkeit entwickelt wurden. Verwendet im 5-Millionen-WebSocket-Verbindungen-Experiment von Oat++. Teil des Oat++ Web-Frameworks.

Implementierungen für C#Edit

  • MindTouch Dream – Das MindTouch Dream REST-Framework bietet eine Implementierung von Coroutines basierend auf dem C# 2.0 Iterator-Pattern
  • Caliburn – Das Caliburn Screen Patterns-Framework für WPF verwendet C# 2.0 Iteratoren, um die UI-Programmierung zu erleichtern, insbesondere in asynchronen Szenarien.
  • Power Threading Library – Die Power Threading Library von Jeffrey Richter implementiert einen AsyncEnumerator, der ein vereinfachtes asynchrones Programmiermodell mit Iterator-basierten Coroutines bietet.
  • Die Unity Game Engine implementiert Coroutines.
  • Servelat Pieces – Das Servelat Pieces Projekt von Yevhen Bobrov bietet transparente Asynchronität für Silverlight WCF Services und die Möglichkeit, jede synchrone Methode asynchron aufzurufen. Die Implementierung basiert auf Caliburns Coroutines-Iterator und C#-Iteratorblöcken.
  • – Das .NET 2.0+ Framework bietet nun Semi-Coroutine (Generator)-Funktionalität durch das Iterator-Muster und das yield-Schlüsselwort.

C# 5.0 enthält Unterstützung für die await-Syntax.

Implementierungen für ClojureEdit

Cloroutine ist eine Bibliothek eines Drittanbieters, die Unterstützung für stacklose Coroutines in Clojure bietet. Sie ist als Makro implementiert, das einen beliebigen Codeblock bei beliebigen var-Aufrufen statisch aufspaltet und die Coroutine als zustandsabhängige Funktion ausgibt.

Implementierungen für DEdit

D implementiert Coroutines als seine Standardbibliotheksklasse Fiber Ein Generator macht es trivial, eine Fiber-Funktion als Eingabebereich darzustellen, was jede Fiber mit bestehenden Bereichsalgorithmen kompatibel macht.

Implementierungen für JavaEdit

Es gibt mehrere Implementierungen für Coroutines in Java. Trotz der Einschränkungen, die durch die Abstraktionen von Java auferlegt werden, schließt die JVM diese Möglichkeit nicht aus. Es gibt vier allgemeine Methoden, von denen zwei die Bytecode-Portabilität zwischen standardkonformen JVMs unterbrechen.

  • Modifizierte JVMs. Es ist möglich, eine gepatchte JVM zu bauen, um Coroutines nativer zu unterstützen. Für die Da Vinci JVM wurden bereits Patches erstellt.
  • Modifizierter Bytecode. Coroutine-Funktionalität ist durch das Umschreiben von regulärem Java-Bytecode möglich, entweder on the fly oder zur Kompilierzeit. Zu den Toolkits gehören Javaflow, Java Coroutines und Coroutines.
  • Plattformspezifische JNI-Mechanismen. Diese verwenden JNI-Methoden, die in den Betriebssystem- oder C-Bibliotheken implementiert sind, um die Funktionalität für die JVM bereitzustellen.
  • Thread-Abstraktionen. Coroutine-Bibliotheken, die unter Verwendung von Threads implementiert werden, können schwergewichtig sein, obwohl die Leistung je nach Thread-Implementierung der JVM variiert.

Implementierungen in JavaScriptEdit

  • node-fibers
    • Fibjs – fibjs ist eine JavaScript-Laufzeitumgebung, die auf der V8 JavaScript-Engine von Chrome aufbaut. fibjs nutzt fibers-switch, sync style und non-blocking I/O model, um skalierbare Systeme zu bauen.
  • Seit ECMAScript 2015 wird stackless coroutine functionality durch „generators“ und yield expressions bereitgestellt.

Implementierungen für KotlinEdit

Kotlin implementiert coroutines als Teil einer first-party library.

Implementierungen für Modula-2Edit

Modula-2, wie von Wirth definiert, implementiert Coroutines als Teil der Standard-SYSTEM-Bibliothek.

Die Prozedur NEWPROCESS() füllt einen Kontext mit einem Codeblock und Platz für einen Stack als Parameter, und die Prozedur TRANSFER() übergibt die Kontrolle an eine Coroutine mit dem Kontext der Coroutine als Parameter.

Implementierung in MonoEdit

Die Mono Common Language Runtime unterstützt Continuations, aus denen Coroutines erstellt werden können.

Implementierung im .NET Framework als FibresEdit

Während der Entwicklung des .NET Framework 2.0 hat Microsoft das Design der Common Language Runtime (CLR) Hosting-APIs erweitert, um Fibre-basiertes Scheduling mit Blick auf die Verwendung im Fibre-Mode für SQL Server zu behandeln. Vor der Veröffentlichung wurde die Unterstützung für den Task-Switching-Hook ICLRTask::SwitchOut aus Zeitgründen entfernt, so dass die Verwendung der Fibre-API zum Umschalten von Tasks derzeit keine praktikable Option im .NET Framework ist.

Implementierungen für PerlEdit

  • Coro

Coroutinen sind nativ in allen Raku-Backends implementiert.

Implementierungen für PHPEdit

  • Amphp
  • Coroutine implementiert in einer Art und Weise, die Python-Funktionen ähnelt, und einige Go, viele Beispiele zeigen, dass es Code mit der gleichen Anzahl von Zeilen und Verhalten konvertiert.

Implementierungen für PythonEdit

  • Python 2.5 implementiert bessere Unterstützung für Coroutine-ähnliche Funktionalität, basierend auf erweiterten Generatoren (PEP 342)
  • Python 3.3 verbessert diese Fähigkeit, indem es die Delegierung an einen Subgenerator unterstützt (PEP 380)
  • Python 3.4 führt ein umfassendes asynchrones E/A-Framework ein, das in PEP 3156 standardisiert ist und Coroutinen enthält, die die Delegation an einen Subgenerator nutzen
  • Python 3.5 führt explizite Unterstützung für Coroutinen mit async/await-Syntax ein (PEP 0492).
  • Seit Python 3.7 sind async/await reservierte Schlüsselwörter.
  • Eventlet
  • Greenlet
  • gevent
  • stackless python
  • Abandoned
    • Shrapnel (letztes Release 2015)
    • Kamaelia (letztes Release 2010)
    • cogen (letztes Release 2009)
    • multitask (letztes Release 2007)
    • chiral

Implementierungen für RubyEdit

  • Ruby 1.9 unterstützt nativ Coroutinen, die als Fibers, also Semi-Coroutinen, implementiert sind.
  • Eine Implementierung von Marc De Scheemaecker
  • Ruby 2.5 und höher unterstützt nativ Coroutines, die als Fasern implementiert sind
  • Eine Implementierung von Thomas W Branson

Implementierungen für RustEdit

Rust unterstützt Coroutines seit Version 1.39 Es gibt auch eine alternative asynchrone Laufzeit (älteres Projekt als die Standardlaufzeit von Rust) : tokio

Implementierungen für ScalaEdit

Scala Coroutines ist eine Coroutine-Implementierung für Scala. Diese Implementierung ist eine Erweiterung auf Bibliotheksebene, die sich auf das Scala-Makrosystem stützt, um Programmteile statisch in Coroutine-Objekte zu verwandeln. Da diese Implementierung keine Änderungen in der JVM erfordert, ist sie vollständig portabel zwischen verschiedenen JVMs und funktioniert mit alternativen Scala-Backends, wie Scala.js, das zu JavaScript kompiliert.

Scala-Coroutinen basieren auf dem Makro coroutine, das einen normalen Codeblock in eine Coroutine-Definition umwandelt. Eine solche Coroutine-Definition kann mit der Operation call aufgerufen werden, die einen Coroutine-Frame instanziiert. Ein Coroutine-Frame kann mit der Methode resume wieder aufgenommen werden, die die Ausführung des Coroutine-Körpers fortsetzt, bis ein yieldval-Schlüsselwort erreicht wird, das den Coroutine-Frame unterbricht. Scala-Coroutinen verfügen auch über eine snapshot-Methode, die die Coroutine effektiv dupliziert. Eine detaillierte Beschreibung von Scala-Coroutinen mit Snapshots wurde auf der ECOOP 2018 zusammen mit ihrem formalen Modell veröffentlicht.

Implementierungen für SchemeEdit

Da Scheme volle Unterstützung für Fortsetzungen bietet, ist die Implementierung von Coroutines fast trivial, da nur eine Warteschlange von Fortsetzungen gepflegt werden muss.

Implementierungen für SmalltalkEdit

Da in den meisten Smalltalk-Umgebungen der Ausführungsstapel ein Bürger erster Klasse ist, können Coroutines ohne zusätzliche Bibliothek oder VM-Unterstützung implementiert werden.

Implementierungen für SwiftEdit

  • SwiftCoroutine – Swift-Coroutine-Bibliothek für iOS, macOS und Linux.

Implementierung für Tool Command Language (Tcl)Edit

Seit Version 8.6 unterstützt die Tool Command Language Coroutines in der Kernsprache.

Implementierungen für ValaEdit

Vala implementiert native Unterstützung für Coroutines. Sie sind für die Verwendung mit einer Gtk-Hauptschleife konzipiert, können aber auch allein verwendet werden, wenn sichergestellt wird, dass der End-Callback nie aufgerufen werden muss, bevor mindestens ein Yield ausgeführt wurde.

Implementierungen in AssemblersprachenBearbeiten

Maschinenabhängige Assemblersprachen bieten oft direkte Methoden für die Ausführung von Coroutines. In MACRO-11, der Assemblersprache der PDP-11-Minicomputer-Familie, erfolgt der „klassische“ Coroutine-Switch beispielsweise durch den Befehl „JSR PC,@(SP)+“, der zu der vom Stack gepoppten Adresse springt und die aktuelle (d.h. die der nächsten) Befehlsadresse auf den Stack schiebt. Auf VAXen (in Macro-32) lautet der vergleichbare Befehl „JSB @(SP)+“. Auch auf einem Motorola 6809 gibt es den Befehl „JSR „; man beachte das „++“, da 2 Bytes (der Adresse) vom Stack gepoppt werden. Diese Anweisung wird häufig im (Standard-) ‚Monitor‘-Assist 09 verwendet.

Schreibe einen Kommentar