Die massive Popularität des Begriffs "asynchron" war in der Welle von Web 2.0, die das Web mit JavaScript und Ajax fegte. Asynchron ist jedoch in den meisten Programmiersprachen auf hoher Ebene selten. PHP spiegelt diese Funktion am besten wider: Es blockiert nicht nur asynchron, sondern liefert auch nicht mehrere Threads. PHP wird synchron blockiert. Solche Vorteile sind für Programmierer vorteilhaft, um die Geschäftslogik nacheinander zu schreiben, aber in komplexen Netzwerkanwendungen führt das Blockieren von gleichzeitiger nicht mehr gleichzeitiger.
Auf der Serverseite ist E/A sehr teuer und verteilte E/A ist teurer. Nur wenn das Backend schnell auf Ressourcen reagieren kann, kann die Front-End-Erfahrung besser werden. Node.js ist die erste Plattform, die Asynchron als Hauptprogrammiermethode und Designkonzept verwendet. Begleitet von asynchronem E/O, ereignisorientiertem und einzelner Threading, bilden sie den Ton des Knotens. In diesem Artikel wird vorgestellt, wie der Knoten asynchrone E/O implementiert.
1. Grundkonzepte
"Async" und "nicht blockierende" klingen gleich und in Bezug auf die tatsächlichen Ergebnisse erreichen beide den Zweck der Parallelität. Aus der Sicht des Computerkerns I/O gibt es jedoch nur zwei Möglichkeiten: Blockierung und Nichtblockieren. Asynchron/synchron und blockiert/nicht blockiert sind tatsächlich zwei verschiedene Dinge.
1.1 Blockieren von E/O und nicht blockierender I/O
Eine Funktion des Blockierens von E/A ist, dass Sie nach dem Anruf warten müssen, bis alle Vorgänge auf der Systemkernelebene abgeschlossen sind, bevor der Anruf beendet ist. Wenn Sie als Beispiel das Lesen einer Datei auf der Festplatte verwenden, endet dieser Anruf, nachdem der Systemkernel die Festplattensuche abgeschlossen, Daten liest und Daten in den Speicher kopiert.
Durch das Blockieren von I/A wird die CPU auf die E/A gewartet, die Wartezeit verschwendet, und die Verarbeitungsleistung der CPU kann nicht vollständig genutzt werden. Das Merkmal der nicht blockierenden I/O ist, dass es unmittelbar nach dem Anruf zurückkehrt und das CPU-Zeitschicht verwendet werden kann, um andere Transaktionen nach der Rückkehr zu verarbeiten. Da die vollständige E/A nicht abgeschlossen ist, ist das, was sofort zurückgegeben wird, nicht die von der Geschäftsschicht erwarteten Daten, sondern nur der Status des aktuellen Anrufs. Um die vollständigen Daten zu erhalten, muss die Anwendung den E/A -Vorgang wiederholt aufrufen, um zu bestätigen, ob sie abgeschlossen ist (d. H. Umfragen). Umfragetechniken benötigen Folgendes:
1.Lesen: Die Überprüfung des E/A -Status durch wiederholte Anrufe ist die originellste und niedrigste Leistungsmethode
2. Auswahl: Verbesserungen zum Lesen, beurteilen Sie den Ereignisstatus im Dateideskriptor. Der Nachteil ist, dass die maximale Anzahl von Dateideskriptoren begrenzt ist.
3.Poll: Verbesserungen zur Auswahl unter Verwendung verknüpfter Listen, um eine maximale Zahlenbeschränkung zu vermeiden. Wenn jedoch viele Deskriptoren vorhanden sind, ist die Leistung immer noch sehr niedrig
4.epoll: Wenn während der Umfrage kein E/A -Ereignis überprüft wird, schläft es, bis das Ereignis auftritt und es aufweckt. Dies ist der effizienteste E -Ereignis -Benachrichtigungsmechanismus unter Linux.
Umfragen erfüllen die Notwendigkeit einer nicht blockierenden E/A, um die vollständige Datenerfassung zu gewährleisten. Für Anwendungen kann dies jedoch weiterhin als eine Art Synchronisation gelten, da es noch darauf warten muss, dass die I/A-Rückkehr vollständig zurückkehrt. Während des Wartens wird die CPU entweder verwendet, um den Status des Dateideskriptors zu durchqueren oder auf Winterschlaf zu warten, die auf Ereignisse warten.
1.2 Asynchrone I/O in Ideal und Realität
Perfektes asynchrones E/A sollte die Anwendung sein, die einen nicht blockierenden Anruf initiiert und die nächste Aufgabe ohne Abfragen direkt erledigen kann. Übergeben Sie die Daten nach Abschluss des E/A.
In Wirklichkeit hat asynchrone I/O unterschiedliche Implementierungen unter verschiedenen Betriebssystemen. Beispielsweise nimmt *NIX -Plattform einen benutzerdefinierten Thread -Pool an, während die Windows -Plattform ein IOCP -Modell übernimmt. Der Knoten liefert Libuv als abstrakte Kapselungsschicht, um die Beurteilung der Plattformkompatibilität zu verringern, und stellt sicher, dass die Implementierung der asynchronen E/A des oberen Knotens und der unteren Plattformen unabhängig ist. Es sollte betont werden, dass wir oft erwähnen, dass der Knoten einsthread ist, was nur bedeutet, dass die Ausführung von JavaScript in einem einzelnen Thread liegt, und es gibt andere Threadpools, die tatsächlich E/A-Aufgaben innerhalb des Knotens erledigen.
2. Asynchrones E/O des Knotens
2.1 Ereignisschleife
Das Ausführungsmodell des Knotens ist eigentlich eine Ereignisschleife. Wenn der Vorgang beginnt, erstellt der Knoten eine unendliche Schleife, und jeder Prozess der Ausführung des Schleifenkörpers wird zum Zecken. Jeder Tick -Prozess soll prüfen, ob Ereignisse darauf warten, verarbeitet zu werden. In diesem Fall werden die Ereignisse und ihre damit verbundenen Rückruffunktionen entfernt. Wenn es zugeordneten Rückruffunktionen gibt, werden sie ausgeführt und dann wird die nächste Schleife eingegeben. Wenn es keine Ereignisverarbeitung mehr gibt, beenden Sie den Prozess.
2.2 Beobachter
In jeder Ereignisschleife gibt es mehrere Beobachter, und indem wir diese Beobachter fragen, können wir feststellen, ob es Ereignisse gibt, die verarbeitet werden müssen. Die Ereignisschleife ist ein typisches Hersteller-/Verbrauchermodell. Im Knoten stammen Ereignisse hauptsächlich aus Netzwerkanfragen, Datei -E/O usw. Diese Ereignisse verfügen über entsprechende Netzwerk -E/A -Beobachter, Datei -E/A -Beobachter usw. Die Ereignisschleife nimmt das Ereignis vom Beobachter heraus und verarbeitet es.
2.3 Anfrageobjekt
Während des Übergangs von JavaScript zum Kernel -E/A -Operationen gibt es ein Zwischenprodukt namens Anforderungsobjekt. Wenn Sie die einfachste Methode von fs.open () in Windows (eine Datei öffnen und einen Dateideskriptor gemäß den angegebenen Pfad und Parametern erhalten), werden JS-Aufrufe zu integrierten Modulen durch Libuv tatsächlich als UV_FS_Open () -Methode bezeichnet. Während des Anrufprozesses wird ein FSReqWrap -Anforderungsobjekt erstellt, und die aus der JS -Ebene übergebenen Parameter und Methoden werden in diesem Anforderungsobjekt eingekapselt. Die Rückruffunktion, über die wir am meisten besorgt sind, ist in der Eigenschaft von OnCompete_Sym dieses Objekts festgelegt. Nachdem das Objekt eingepackt ist, drücken Sie das FSReqWrap -Objekt in den Thread -Pool und warten Sie auf die Ausführung.
Zu diesem Zeitpunkt kehrt der JS -Anruf sofort zurück, und der JS -Thread kann weiterhin nachfolgende Operationen ausführen. Der aktuelle E/A -Betrieb wartet auf die Ausführung im Thread -Pool, wodurch die erste Stufe des asynchronen Anrufs abgeschlossen wird.
2.4 Rückrufe ausführen
Die Rückrufbenachrichtigung ist die zweite Phase der asynchronen E/A. Nachdem der E/A -Betrieb im Thread -Pool aufgerufen wurde, werden die erhaltenen Ergebnisse gespeichert, und dann wird IOCP darüber informiert, dass der aktuelle Objektbetrieb abgeschlossen wurde und der Thread den Thread -Pool zurückgibt. Während jeder Tick -Ausführung ruft der E/A -Beobachter der Ereignisschleife die entsprechende Methode auf, um zu prüfen, ob im Thread -Pool abgeschlossene Anforderungen vorhanden sind. Wenn es vorhanden ist, wird das Anforderungsobjekt zur Warteschlange des E/A -Observers hinzugefügt und dann als Ereignis verarbeitet.
3. Nicht-I/O-Asynchron-API
Es gibt auch einige asynchrone APIs, die nicht mit E/A im Knoten zusammenhängen, wie z.
3.1 Timer -API
Die APIs auf der Browser -Seite von setTimeout () und setInterval () sind konsistent. Ihr Implementierungsprinzip ähnelt asynchroner E/A, erfordern jedoch nicht die Beteiligung des E/A -Threadpools. Der durch Aufrufen der Timer -API erstellte Timer wird in einen roten und schwarzen Baum im Timer -Observer eingefügt. Jedes Ereignisschleife wird das Timerobjekt vom roten und schwarzen Baum iteriert, um zu überprüfen, ob die Zeitspanne überschritten hat. Wenn es überschreitet, wird ein Ereignis gebildet und die Rückruffunktion wird sofort ausgeführt. Das Hauptproblem bei einem Timer ist, dass seine Zeitverbindungszeit nicht besonders genau ist (Millisekunden, innerhalb der Toleranz).
3.2 ASynchrone Task Execution API
Bevor der Knoten erschien, können viele Menschen dies anrufen, um eine asynchrone Aufgabe sofort auszuführen:
Die Codekopie lautet wie folgt:
setTimeout (function () {
// todo
}, 0);
Aufgrund der Eigenschaften von Ereignisschleifen ist der Timer nicht genau genug, und die Verwendung eines roten und schwarzen Baumes erfordert die Verwendung eines Timers, und die Komplexität verschiedener Betriebszeit ist o (log (n)). Die Methode von Process.NextTick () bringt die Rückruffunktion nur in die Warteschlange ein und nimmt sie in der nächsten Runde von Tick aus. Die Komplexität ist o (1) und sie ist effizienter.
Darüber hinaus gibt es eine SetImmediate () -Methode ähnlich der obigen Methode, wobei beide die Ausführung der Rückruffunktion verzögert werden. Ersteres hat jedoch eine höhere Priorität als die letztere, da der Ereignisschleife den Beobachter nacheinander überprüft. Darüber hinaus wird die frühere Rückruffunktion in einem Array gespeichert, und jede Zeckenrunde wird alle Rückruffunktionen im Array ausführen. Das letztere Ergebnis wird in einer verknüpften Liste gespeichert, und jede Zeckenrunde wird nur eine Rückruffunktion ausführen.
4. Ereignisgesteuerte und Hochleistungsserver
Das vorherige Beispiel zeigt, wie Knoten asynchrones E/O implementiert. Tatsächlich wendet Node auch eine asynchrone E/A für die Verarbeitung von Netzwerk -Socket -Verarbeitung an, was auch die Grundlage für den Knoten zum Erstellen eines Webservers bildet. Klassische Servermodelle sind:
1. Synchron: Es kann jeweils nur eine Anfrage bearbeitet werden, und der Rest der Anfragen befindet sich in einem Wartezustand
2. pro Prozess/pro Anfrage: Starten Sie einen Prozess für jede Anfrage, die Systemressourcen sind jedoch begrenzt und haben keine Skalierbarkeit.
3. pro Thread/pro Anforderung: Starten Sie einen Thread für jede Anforderung. Themen sind leichter als Prozesse, aber jeder Thread nimmt eine bestimmte Menge an Speicher ein. Wenn große gleichzeitige Anfragen ankommen, wird der Speicher bald ausgehen.
Der berühmte Apache übernimmt die Form pro Thread/pro-Request, weshalb es schwierig ist, mit hoher Parallelität umzugehen. Der Knoten erledigt Anfragen mit ereignisgesteuerten Methoden, mit denen das Aufwand des Erstellens und Zerstörens von Threads speichern kann. Gleichzeitig hat das Betriebssystem bei Planungsaufgaben weniger Threads, und die Kosten für die Kontextschaltung sind ebenfalls sehr niedrig. Der Knoten kann Anforderungen auch mit einer großen Anzahl von Verbindungen ordentlich verarbeiten.
Der bekannte Server Nginx verlangt auch die Multi-Threading-Methode und verwendet dieselbe ereignisgesteuerte Methode wie Knoten. Jetzt ist Nginx in großer Weise, um Apache zu ersetzen. Nginx ist in reinem C geschrieben und verfügt über eine hohe Leistung, ist jedoch nur für Webserver geeignet, verwendet für das Reverse -Proxying oder für das Lastausgleich usw. Der Knoten kann die gleichen Funktionen wie Nginx erstellen und auch verschiedene spezifische Unternehmen bewältigen, und seine eigene Leistung ist auch gut. In tatsächlichen Projekten können wir sie kombinieren, um die beste Leistung der Anwendung zu erzielen.