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.