Warum sind Coroutinen zurück? [geschlossen]

19

Die meisten Vorarbeiten für Koroutinen erfolgten in den 60er / 70er Jahren und wurden dann zugunsten von Alternativen (z. B. Fäden) eingestellt.

Gibt es irgendeinen Grund für das erneute Interesse an Koroutinen, das in Python und anderen Sprachen aufgetreten ist?

user1787812
quelle
9
Ich bin nicht sicher, ob sie jemals gegangen sind.
Blrfl

Antworten:

26

Coroutinen sind nie gegangen, sie wurden in der Zwischenzeit nur von anderen Dingen überschattet. Das in letzter Zeit gestiegene Interesse an asynchroner Programmierung und damit an Koroutinen ist im Wesentlichen auf drei Faktoren zurückzuführen: die zunehmende Akzeptanz funktionaler Programmiertechniken, Toolsets mit unzureichender Unterstützung für echte Parallelität (JavaScript! Python!) Und vor allem die unterschiedlichen Kompromisse zwischen Threads und Koroutinen. Für einige Anwendungsfälle sind Coroutinen objektiv besser.

Eines der größten Programmierparadigmen der 80er, 90er und heute ist OOP. Wenn wir uns die Geschichte von OOP und insbesondere die Entwicklung der Simula-Sprache ansehen, sehen wir, dass Klassen aus Koroutinen entstanden sind. Simula war für die Simulation von Systemen mit diskreten Ereignissen gedacht. Jedes Element des Systems war ein separater Prozess, der als Reaktion auf Ereignisse für die Dauer eines Simulationsschritts ausgeführt wurde und dann anderen Prozessen die Arbeit überließ. Während der Entwicklung von Simula 67 wurde das Klassenkonzept eingeführt. Jetzt wird der permanente Zustand der Coroutine in den Objektelementen gespeichert und Ereignisse werden durch Aufrufen einer Methode ausgelöst. Weitere Informationen finden Sie in dem Artikel Die Entwicklung der SIMULA-Sprachen von Nygaard & Dahl.

In einer witzigen Wendung haben wir die ganze Zeit Koroutinen verwendet, wir haben sie nur Objekte und ereignisgesteuerte Programmierung genannt.

In Bezug auf die Parallelität gibt es zwei Arten von Sprachen: diejenigen, die ein geeignetes Speichermodell haben, und diejenigen, die dies nicht tun. Ein Speichermodell beschreibt Dinge wie: „Wenn ich in eine Variable schreibe und danach von dieser Variable in einem anderen Thread lese, sehe ich den alten oder den neuen Wert oder vielleicht einen ungültigen Wert? Was bedeutet "vor" und "nach"? Welche Operationen sind garantiert atomar? “

Das Erstellen eines guten Speichermodells ist schwierig, daher wurden diese Anstrengungen für die meisten dieser nicht spezifizierten, implementierungsdefinierten dynamischen Open-Source-Sprachen (Perl, JavaScript, Python, Ruby, PHP) nie unternommen. Natürlich haben sich all diese Sprachen weit über das „Scripting“ hinaus entwickelt, für das sie ursprünglich entwickelt wurden. Nun, einige dieser Sprachen haben eine Art Speichermodelldokument, aber diese reichen nicht aus. Stattdessen haben wir Hacks:

  • Perl kann mit Threading-Unterstützung kompiliert werden, aber jeder Thread enthält einen separaten Klon des vollständigen Interpreter-Status, wodurch Threads unerschwinglich teuer werden. Als einziger Vorteil vermeidet dieser Shared-Nothing-Ansatz Datenrennen und zwingt Programmierer, nur über Warteschlangen / Signale / IPC zu kommunizieren. Perl hat keine gute Geschichte für die asynchrone Verarbeitung.

  • JavaScript hatte schon immer eine umfassende Unterstützung für die funktionale Programmierung, sodass Programmierer Fortsetzungen / Rückrufe in ihren Programmen manuell codierten, wenn sie asynchrone Vorgänge benötigten. Zum Beispiel bei Ajax-Anfragen oder Animationsverzögerungen. Da das Web von Natur aus asynchron ist, gibt es viel asynchronen JavaScript-Code, und die Verwaltung all dieser Rückrufe ist äußerst schmerzhaft. Wir sehen daher viele Anstrengungen, um diese Rückrufe besser zu organisieren (Versprechen) oder sie vollständig zu beseitigen.

  • Python hat diese unglückliche Funktion, die als Global Interpreter Lock bezeichnet wird. Grundsätzlich lautet das Python-Speichermodell: „Alle Effekte werden nacheinander angezeigt, da keine Parallelität besteht. Es wird immer nur ein Thread Python-Code ausführen. “Python verfügt zwar über Threads, diese sind jedoch nur so leistungsfähig wie Coroutinen. [1] Python kann über Generatorfunktionen mit vielen Coroutinen kodieren yield. Bei richtiger Verwendung kann dies allein den größten Teil der von JavaScript bekannten Rückrufhölle vermeiden. Das neuere asynchrone / wartende System von Python 3.5 vereinfacht asynchrone Redewendungen in Python und integriert eine Ereignisschleife.

    [1]: Technisch gesehen gelten diese Einschränkungen nur für CPython, die Python-Referenzimplementierung. Andere Implementierungen wie Jython bieten echte Threads, die parallel ausgeführt werden können, aber eine große Länge haben müssen, um ein gleichwertiges Verhalten zu implementieren. Im Wesentlichen: Jede Variable oder jedes Objektelement ist eine flüchtige Variable, sodass alle Änderungen atomar sind und sofort in allen Threads angezeigt werden. Die Verwendung flüchtiger Variablen ist natürlich weitaus teurer als die Verwendung normaler Variablen.

  • Ich weiß nicht genug über Ruby und PHP, um sie richtig zu rösten.

Zusammenfassend lässt sich sagen, dass einige dieser Sprachen grundlegende Entwurfsentscheidungen haben, die Multithreading unerwünscht oder unmöglich machen. Dies führt zu einem stärkeren Fokus auf Alternativen wie Coroutinen und Möglichkeiten, die asynchrone Programmierung komfortabler zu gestalten.

Lassen Sie uns abschließend über die Unterschiede zwischen Coroutinen und Threads sprechen:

Threads ähneln im Wesentlichen Prozessen, mit der Ausnahme, dass sich mehrere Threads innerhalb eines Prozesses einen Speicherplatz teilen. Dies bedeutet, dass Threads in Bezug auf den Speicher keineswegs „leicht“ sind. Threads werden vom Betriebssystem vorab geplant. Dies bedeutet, dass Task-Switches einen hohen Overhead haben und zu unpraktischen Zeiten auftreten können. Dieser Overhead besteht aus zwei Komponenten: den Kosten für die Unterbrechung des Thread-Status und den Kosten für den Wechsel zwischen Benutzermodus (für den Thread) und Kernel-Modus (für den Scheduler).

Wenn ein Prozess seine eigenen Threads direkt und kooperativ plant, ist die Kontextumschaltung in den Kernelmodus nicht erforderlich, und das Umschalten von Tasks ist vergleichbar teuer mit einem indirekten Funktionsaufruf, wie in: ziemlich billig. Diese leichten Fäden können in Abhängigkeit von verschiedenen Details als grüne Fäden, Fasern oder Coroutinen bezeichnet werden. Bemerkenswerte Benutzer von grünen Fäden / Fasern waren frühe Java-Implementierungen und in jüngerer Zeit Goroutines in Golang. Ein konzeptioneller Vorteil von Koroutinen besteht darin, dass ihre Ausführung im Sinne eines Steuerflusses verstanden werden kann, der explizit zwischen Koroutinen hin und her geht. Diese Coroutinen erreichen jedoch keine echte Parallelität, es sei denn, sie sind für mehrere Betriebssystemthreads geplant.

Wo sind billige Coroutinen nützlich? Die meiste Software benötigt keine Millionen Threads, daher sind normale, teure Threads normalerweise in Ordnung. Die asynchrone Programmierung kann jedoch manchmal Ihren Code vereinfachen. Um frei verwendet zu werden, muss diese Abstraktion ausreichend billig sein.

Und dann ist da noch das Web. Wie oben erwähnt, ist das Web von Natur aus asynchron. Netzwerkanfragen dauern einfach lange. Viele Webserver unterhalten einen Thread-Pool voller Worker-Threads. In den meisten Fällen sind diese Threads jedoch inaktiv, da sie auf eine Ressource warten, z. B. auf ein E / A-Ereignis beim Laden einer Datei von der Festplatte, auf die Bestätigung eines Teils der Antwort durch den Client oder auf eine Datenbank Abfrage abgeschlossen. NodeJS hat auf phänomenale Weise gezeigt, dass ein konsequentes ereignisbasiertes und asynchrones Serverdesign sehr gut funktioniert. Offensichtlich ist JavaScript bei weitem nicht die einzige Sprache, die für Webanwendungen verwendet wird. Daher gibt es auch einen großen Anreiz für andere Sprachen (erkennbar in Python und C #), die asynchrone Webprogrammierung zu vereinfachen.

amon
quelle
Ich würde empfehlen, Ihren vierten bis letzten Absatz zu verwenden, um das Risiko von Plagiaten zu vermeiden. Er ist nahezu identisch mit einer anderen Quelle, die ich gelesen habe. Zusätzlich kann die Leistung von Coroutinen nicht zu einem "indirekten Funktionsaufruf" vereinfacht werden, obwohl der Overhead um Größenordnungen geringer als der von Threads ist. Weitere Informationen zu Coroutine-Implementierungen finden Sie hier und hier .
Uhr
1
@snb In Bezug auf Ihre vorgeschlagene Bearbeitung: Die GIL kann ein CPython-Implementierungsdetail sein, das grundlegende Problem ist jedoch, dass die Python- Sprache kein explizites Speichermodell hat, das die parallele Mutation von Daten spezifiziert. Die GIL ist ein Hack, um diese Probleme zu umgehen. Python-Implementierungen mit echter Parallelität müssen jedoch große Anstrengungen unternehmen, um eine gleichwertige Semantik bereitzustellen, z. B. wie im Jython-Buch beschrieben . Grundsätzlich gilt: Jede Variable oder jedes Objektfeld muss eine teure flüchtige Variable sein.
Amon
3
@snb In Bezug auf Plagiate: Plagiate präsentieren fälschlicherweise Ideen als Ihre eigenen, insbesondere im akademischen Kontext. Es ist eine schwerwiegende Anschuldigung , aber ich bin sicher, dass Sie es nicht so gemeint haben. Der Abschnitt „Threads sind im Grunde genommen wie Prozesse“ wiederholt lediglich bekannte Fakten, wie sie in jeder Vorlesung oder jedem Lehrbuch über Betriebssysteme vermittelt werden. Da es nur so viele Möglichkeiten gibt, diese Fakten präzise auszudrücken, wundert es mich nicht, dass der Absatz Ihnen bekannt vorkommt.
Amon
Ich habe nicht den Sinn ändern zu implizieren , dass Python hat ein Speichermodell. Auch die Verwendung von volatile verringert die Performance nicht von sich aus. Volatile bedeutet lediglich, dass der Compiler die Variable nicht so optimieren kann, dass davon ausgegangen werden kann, dass die Variable im aktuellen Kontext ohne explizite Operationen unverändert bleibt. In der Jython-Welt spielt dies möglicherweise eine Rolle, da die VM JIT-Kompilierung verwendet wird. In der CPython-Welt sorgen Sie sich jedoch nicht um die JIT-Optimierung. Ihre flüchtigen Variablen befinden sich im Laufzeitbereich des Interpreters, in dem keine Optimierungen vorgenommen werden konnten .
WHN
7

Coroutines waren früher nützlich, weil Betriebssysteme keine präventive Planung durchführten. Sobald sie mit der vorbeugenden Planung begannen, war es länger notwendig, die Kontrolle in Ihrem Programm regelmäßig aufzugeben.

Mit zunehmender Verbreitung von Multi-Core-Prozessoren werden Coroutinen verwendet, um Task-Parallelität zu erreichen und / oder die Auslastung eines Systems hoch zu halten (wenn ein Ausführungsthread auf eine Ressource warten muss, kann ein anderer an seiner Stelle ausgeführt werden).

NodeJS ist ein Sonderfall, bei dem Coroutinen verwendet werden, um parallel auf IO zuzugreifen. Das heißt, mehrere Threads werden verwendet, um E / A-Anforderungen zu bearbeiten, aber ein einzelner Thread wird verwendet, um den Javascript-Code auszuführen. Der Zweck der Ausführung eines Benutzercodes in einem Signatur-Thread besteht darin, die Verwendung von Mutexen zu vermeiden. Dies fällt unter die Kategorie des Versuchs, die Auslastung des Systems hoch zu halten, wie oben erwähnt.

dlasalle
quelle
4
Coroutinen werden jedoch nicht vom Betriebssystem verwaltet. OS weiß nicht, was eine Coroutine ist, im Gegensatz zu C ++
Überaustausch
Viele Betriebssysteme haben Coroutinen.
Jörg W Mittag
Coroutinen wie Python und Javascript ES6 + sind aber keine Multiprozessoren? Wie erreichen diese Aufgabenparallelität?
6.
1
@Mael Die jüngste "Wiederbelebung" der Coroutinen geht auf Python und Javascript zurück, die beide, wie ich verstehe, keine Parallelität mit ihren Coroutinen erreichen. Das heißt, dass diese Antwort falsch ist, da Aufgabenparallismus nicht der Grund ist, warum Koroutinen überhaupt "zurück" sind. Auch Luas sind nicht Multiprozess? EDIT: Mir ist gerade aufgefallen, dass Sie nicht über Parallelität gesprochen haben, aber warum haben Sie mir überhaupt geantwortet? Antworte dlasalle, da sie eindeutig falsch liegen.
6.
3
@dlasalle Nein, sie können es nicht, obwohl "parallel laufen" steht, was nicht bedeutet, dass Code physisch zur gleichen Zeit ausgeführt wird. GIL würde es stoppen und Async erzeugt keine separaten Prozesse, die für die Mehrfachverarbeitung in CPython erforderlich sind (separate GILs). Async arbeitet mit Erträgen auf einem einzelnen Thread. Wenn sie "parralel" sagen, meinen sie tatsächlich mehrere Funktionen, die sich auf andere Funktionen auswirken, und die Ausführung von Interleving- Funktionen. Python-Async-Prozesse können aufgrund von impl nicht parallel ausgeführt werden. Ich habe jetzt drei Sprachen, die keine Parralel-Coroutinen beherrschen, Lua, Javascript und Python.
Uhr
5

Frühe Systeme verwendeten Coroutinen, um Parallelität bereitzustellen, vor allem, weil sie der einfachste Weg sind, dies zu tun. Threads erfordern eine angemessene Menge an Unterstützung vom Betriebssystem (Sie können sie auf Benutzerebene implementieren, müssen jedoch so eingerichtet werden, dass das System Ihren Prozess in regelmäßigen Abständen unterbricht), und sind schwieriger zu implementieren, selbst wenn Sie über die Unterstützung verfügen .

Threads übernahmen später, weil sie in den 70ern oder 80ern von allen ernsthaften Betriebssystemen unterstützt wurden (und in den 90ern sogar von Windows!) Und allgemeiner. Und sie sind einfacher zu bedienen. Plötzlich dachten alle, Fäden seien das nächste große Ding.

In den späten 90ern begannen Risse aufzutreten, und in den frühen 2000ern stellte sich heraus, dass es ernsthafte Probleme mit Gewinden gab:

  1. Sie verbrauchen eine Menge Ressourcen
  2. Kontextwechsel nehmen relativ viel Zeit in Anspruch und sind oft unnötig
  3. sie zerstören die Bezugslokalität
  4. Es ist unerwartet schwierig, den richtigen Code zu schreiben, der mehrere Ressourcen koordiniert, für die möglicherweise exklusiver Zugriff erforderlich ist

Im Laufe der Zeit hat die Anzahl der Aufgaben, die Programme zu einem bestimmten Zeitpunkt ausführen müssen, rapide zugenommen, was die durch (1) und (2) oben verursachten Probleme vergrößert. Die Diskrepanz zwischen Prozessorgeschwindigkeit und Speicherzugriffszeiten hat zugenommen, was das Problem verschärft (3). Die Komplexität der Programme in Bezug auf die Anzahl und die verschiedenen Arten von Ressourcen, die sie benötigen, hat zugenommen und die Relevanz des Problems erhöht (4).

Indem Sie jedoch ein wenig an Allgemeingültigkeit verlieren und den Programmierer ein wenig mehr damit beauftragen, darüber nachzudenken, wie ihre Prozesse zusammenarbeiten können, können Coroutinen all diese Probleme lösen.

  1. Coroutinen erfordern wenig mehr Ressourcen als eine Handvoll Seiten für den Stapel, viel weniger als die meisten Implementierungen von Threads.
  2. Coroutinen wechseln den Kontext nur an vom Programmierer definierten Punkten, was hoffentlich nur dann bedeutet, wenn es notwendig ist. Außerdem müssen sie normalerweise nicht so viele Kontextinformationen (z. B. Registerwerte) wie Threads beibehalten, was bedeutet, dass jeder Switch in der Regel schneller ist und weniger benötigt.
  3. Gängige Koroutinenmuster, einschließlich Operationen vom Typ Produzent / Konsument, geben Daten zwischen Routinen auf eine Weise weiter, die die Lokalität aktiv erhöht. Darüber hinaus treten Kontextwechsel typischerweise nur zwischen Arbeitseinheiten auf, die sich nicht in ihnen befinden, dh zu einer Zeit, in der die Lokalität normalerweise sowieso minimiert ist.
  4. Das Sperren von Ressourcen ist weniger wahrscheinlich, wenn Routinen wissen, dass sie während eines Vorgangs nicht willkürlich unterbrochen werden können, sodass einfachere Implementierungen ordnungsgemäß funktionieren.
Jules
quelle
5

Vorwort

Ich möchte damit beginnen, einen Grund anzugeben, warum Koroutinen keine Wiederbelebung und Parallelität bekommen. Im Allgemeinen sind moderne Coroutinen kein Mittel, um aufgabenbasierte Parallelität zu erreichen, da moderne Implementierungen keine Multiprozessierungsfunktionalität verwenden. Das, was Sie dem am nächsten kommen, sind Dinge wie Fasern .

Modern Usage (warum sie zurück sind)

Moderne Koroutinen wurden entwickelt, um eine verzögerte Auswertung zu erreichen , was in funktionalen Sprachen wie haskell sehr nützlich ist. Anstatt einen gesamten Satz zu durchlaufen, um eine Operation auszuführen, können Sie nur eine Auswertung so oft wie nötig durchführen ( nützlich für unendliche Mengen von Gegenständen oder auf andere Weise große Mengen mit vorzeitiger Beendigung und Teilmengen).

Mit der Verwendung des Yield-Schlüsselworts zum Erstellen von Generatoren (die an sich einen Teil der verzögerten Evaluierungsanforderungen erfüllen) in Sprachen wie Python und C # waren Coroutinen in der modernen Implementierung nicht nur möglich, sondern auch ohne spezielle Syntax in der Sprache selbst (obwohl Python schließlich ein paar Bits hinzugefügt hat, um zu helfen). Co-Routinen Hilfe bei faul evaulation mit der Idee der Zukunft s , wo , wenn Sie den Wert einer Variablen nicht zu dieser Zeit benötigen, können Sie es tatsächlich zu erwerben verzögern können , bis Sie explizit für diesen Wert stellen (so dass Sie den Wert verwenden und faul auswerten zu einem anderen Zeitpunkt als Instanziierung).

Abgesehen von der schleppenden Auswertung, helfen diese Co-Routinen vor allem in der Websphäre dabei, die Callback-Hölle zu reparieren . Coroutinen werden beim Datenbankzugriff, bei Online-Transaktionen usw. nützlich, wenn die Verarbeitungszeit auf dem Client-Computer selbst nicht zu einem schnelleren Zugriff auf das führt, was Sie benötigen. Threading könnte das Gleiche erfüllen, erfordert jedoch viel mehr Aufwand in diesem Bereich und ist im Gegensatz zu Coroutinen tatsächlich für die Aufgabenparallelität nützlich .

Kurz gesagt, da die Webentwicklung wächst und funktionale Paradigmen immer mehr mit imperativen Sprachen verschmelzen, haben sich Koroutinen als Lösung für asynchrone Probleme und verzögerte Evaluierung erwiesen. Coroutinen kommen in Problembereiche, in denen Multiprozess-Threading und Threading im Allgemeinen entweder unnötig, unpraktisch oder nicht möglich sind.

Modernes Beispiel

Coroutinen in Sprachen wie Javascript, Lua, C # und Python leiten ihre Implementierungen durch einzelne Funktionen ab , die die Steuerung des Hauptthreads an andere Funktionen abgeben (nichts mit Betriebssystemaufrufen zu tun).

In diesem Python-Beispiel haben wir eine lustige Python-Funktion, in der etwas aufgerufen awaitwird. Dies ist im Grunde genommen eine Ausbeute, die eine Ausführung für die ergibt, loopdie dann die Ausführung einer anderen Funktion ermöglicht (in diesem Fall einer anderen factorialFunktion). Beachten Sie, dass bei der Angabe "Parallele Ausführung von Tasks", die eine falsche Bezeichnung darstellt, die Ausführung der Interleaving- Funktion nicht parallel erfolgt, sondern über das Schlüsselwort await (dies ist nur eine besondere Art der Ausgabe).

Sie ermöglichen einzelne, nicht parallele Kontrollausbeuten für gleichzeitige Prozesse, die nicht aufgabenparallel sind , in dem Sinne, dass diese Aufgaben niemals gleichzeitig ausgeführt werden. Coroutinen sind in modernen Sprachimplementierungen keine Threads. Alle diese Sprachimplementierungen von Co-Routinen werden von diesen Funktionsausbeute-Aufrufen abgeleitet (die Sie als Programmierer tatsächlich manuell in Ihre Co-Routinen einfügen müssen).

BEARBEITEN: C ++ Boost Coroutine2 funktioniert auf die gleiche Weise, und ihre Erklärung sollte ein besseres Bild von dem geben, wovon ich mit yeilds spreche, siehe hier . Wie Sie sehen, gibt es bei den Implementierungen keinen "Sonderfall", Dinge wie Boost-Fasern sind die Ausnahme von der Regel und erfordern sogar dann eine explizite Synchronisation.

EDIT2: Da jemand dachte, ich spreche über C # Task-basiertes System, war ich nicht. Ich sprach über Unitys System und naive C # -Implementierungen

wie
quelle
@T.Sar Ich habe nie gesagt, dass C # "natürliche" Coroutinen hat, C ++ (könnte sich ändern) und Python (und es hatte sie immer noch) auch nicht, und alle drei haben Co-Routine-Implementierungen. Aber alle C # -Implementierungen von Coroutinen (wie die in Unity) basieren auf dem von mir beschriebenen Ertrag. Auch Ihre Verwendung von "Hack" hier ist bedeutungslos, ich denke, jedes Programm ist ein Hack, weil es nicht immer in der Sprache definiert war. Ich verwechsle C # "aufgabenbasiertes System" in keiner Weise mit irgendetwas, ich habe es nicht einmal erwähnt.
Uhr
Ich würde vorschlagen, Ihre Antwort etwas klarer zu gestalten. C # verfügt sowohl über das Konzept der Warteanweisungen als auch über ein aufgabenbasiertes Parallelitätssystem - die Verwendung von C # und diesen Wörtern sowie die Angabe von Beispielen zu Python darüber, dass Python nicht wirklich wirklich parallel ist, kann viel Verwirrung stiften. Entfernen Sie auch Ihren ersten Satz - es ist nicht erforderlich, andere Benutzer in einer solchen Antwort direkt anzugreifen.
T. Sar - Reinstate Monica