Der Begriff „asynchron“ wurde in der Web 2.0-Welle weit verbreitet, die zusammen mit Javascript und AJAX das Web erfasste. In den meisten höheren Programmiersprachen ist Asynchronität jedoch selten. PHP verkörpert diese Funktion am besten: Es blockiert nicht nur asynchron, sondern bietet nicht einmal Multithreading. PHP wird synchron blockierend ausgeführt. Dieser Vorteil hilft Programmierern, Geschäftslogik sequentiell zu schreiben, aber in komplexen Netzwerkanwendungen verhindert das Blockieren eine bessere Parallelität.
Auf der Serverseite ist E/A sehr teuer, und verteilte E/A ist noch teurer. Nur wenn das Backend schnell auf Ressourcen reagieren kann, kann das Front-End-Erlebnis verbessert werden. Node.js ist die erste Plattform, die asynchrone als Hauptprogrammierungsmethode und Designkonzept verwendet. Neben asynchroner E/A gibt es ereignisgesteuerte und Single-Threaded-Operationen, die den Ton von Node bilden. In diesem Artikel wird vorgestellt, wie Node asynchrone E/A implementiert.
1. Grundkonzepte
„Asynchron“ und „nicht blockierend“ klingen wie dasselbe. In Bezug auf die praktischen Auswirkungen erreichen beide den Zweck der Parallelität. Aus Sicht der Computerkernel-E/A gibt es jedoch nur zwei Methoden: Blockieren und Nichtblockieren. Asynchron/synchron und blockierend/nicht blockierend sind also eigentlich zwei verschiedene Dinge.
1.1 Blockierende E/A und nicht blockierende E/A
Ein Merkmal des Blockierens von E/A besteht darin, dass der Aufruf nach dem Aufruf warten muss, bis alle Vorgänge auf Systemkernebene abgeschlossen sind, bevor der Aufruf beendet wird. Am Beispiel des Lesens einer Datei auf der Festplatte endet dieser Aufruf, nachdem der Systemkern die Festplattensuche abgeschlossen, die Daten gelesen und in den Speicher kopiert hat.
Das Blockieren von E/A führt dazu, dass die CPU auf E/A wartet, wodurch Wartezeit verschwendet wird und die Verarbeitungsleistung der CPU nicht vollständig genutzt werden kann. Das Merkmal nicht blockierender E/A besteht darin, dass sie unmittelbar nach dem Aufruf zurückgegeben wird. Nach der Rückkehr kann die CPU-Zeitscheibe für die Verarbeitung anderer Transaktionen verwendet werden. Da die vollständige E/A noch nicht abgeschlossen ist, werden nicht die von der Business-Schicht erwarteten Daten sofort zurückgegeben, sondern nur der Status des aktuellen Aufrufs. Um vollständige Daten zu erhalten, muss die Anwendung den E/A-Vorgang wiederholt aufrufen, um zu bestätigen, ob er abgeschlossen ist (dh Abfrage). Zu den Umfragetechniken gehören die folgenden:
1.Lesen: Die Überprüfung des E/A-Status durch wiederholte Aufrufe ist die primitivste und leistungsschwächste Methode
2.Auswahl: Verbesserung des Lesevorgangs, gemessen am Ereignisstatus im Dateideskriptor. Der Nachteil besteht darin, dass die maximale Anzahl an Dateideskriptoren begrenzt ist
3. Umfrage: Verbesserung der Auswahl, Verwendung einer verknüpften Liste, um eine maximale Anzahlbegrenzung zu vermeiden, aber wenn viele Deskriptoren vorhanden sind, ist die Leistung immer noch sehr gering
4.epoll: Wenn beim Aufrufen der Abfrage kein E/A-Ereignis erkannt wird, bleibt es im Ruhezustand, bis ein Ereignis eintritt, das es aufweckt. Dies ist der derzeit effizienteste E/A-Ereignisbenachrichtigungsmechanismus unter Linux
Polling erfüllt den Bedarf an nicht blockierenden E/A, um eine vollständige Datenerfassung sicherzustellen. Für die Anwendung kann es jedoch immer noch nur als eine Art Synchronisierung gezählt werden, da noch auf die Rückkehr der E/A gewartet werden muss vollständig. Während der Wartezeit wird die CPU entweder zum Durchlaufen des Status des Dateideskriptors oder zum Ruhen und Warten auf das Eintreten eines Ereignisses verwendet.
1.2 Asynchrone I/O in Ideal und Realität
Perfekte asynchrone E/A sollte dann vorliegen, wenn die Anwendung einen nicht blockierenden Aufruf initiiert und die nächste Aufgabe ohne Abfrage direkt verarbeiten kann. Sie muss die Daten nur über ein Signal oder einen Rückruf an die Anwendung übergeben, nachdem die E/A erfolgt ist vollendet. .
Asynchrone E/A hat in der Realität unterschiedliche Implementierungen unter verschiedenen Betriebssystemen. Beispielsweise verwendet die *nix-Plattform einen benutzerdefinierten Thread-Pool und die Windows-Plattform verwendet das IOCP-Modell. Der Knoten stellt libuv als abstrakte Kapselungsschicht bereit, um die Beurteilung der Plattformkompatibilität zu kapseln und sicherzustellen, dass die Implementierung asynchroner E/A des oberen Knotens und der unteren Plattform unabhängig ist. Darüber hinaus muss betont werden, dass wir häufig erwähnen, dass Node Single-Threaded ist. Dies bedeutet nur, dass Javascript in einem einzelnen Thread ausgeführt wird. Es gibt einen Thread-Pool, der tatsächlich E/A-Aufgaben in Node ausführt.
2. Asynchrone E/A des Knotens
2.1 Ereignisschleife
Das Ausführungsmodell von Node ist eigentlich eine Ereignisschleife. Wenn der Prozess startet, erstellt Node eine Endlosschleife und jede Ausführung des Schleifenkörpers wird zu einem Tick. Bei jedem Tick-Prozess wird überprüft, ob Ereignisse auf die Verarbeitung warten. Wenn ja, werden das Ereignis und die zugehörigen Rückruffunktionen abgerufen, diese ausgeführt und dann in die nächste Schleife eingetreten. Wenn keine weiteren Ereignisse zu verarbeiten sind, beenden Sie den Prozess.
2.2 Beobachter
In jeder Ereignisschleife gibt es mehrere Beobachter. Sie können feststellen, ob Ereignisse verarbeitet werden müssen, indem Sie diese Beobachter fragen. Die Ereignisschleife ist ein typisches Producer/Consumer-Modell. In Node stammen Ereignisse hauptsächlich aus Netzwerkanforderungen, Datei-E/A usw. Diese Ereignisse verfügen über entsprechende Netzwerk-E/A-Beobachter, Datei-E/A-Beobachter usw. Die Ereignisschleife übernimmt die Ereignisse von den Beobachtern und verarbeitet sie.
2.3 Objekt anfordern
Im Übergangsprozess von Javascript, das einen Aufruf initiiert, zum Kernel, der eine E/A-Operation abschließt, gibt es ein Zwischenprodukt, das als Anforderungsobjekt bezeichnet wird. Am Beispiel der einfachsten fs.open()-Methode unter Windows (um eine Datei zu öffnen und einen Dateideskriptor gemäß dem angegebenen Pfad und den angegebenen Parametern zu erhalten) ruft der Aufruf des Systems von JS zum integrierten Modul über libuv tatsächlich uv_fs_open( ) Methode. Während des Aufrufvorgangs wird ein FSReqWrap-Anforderungsobjekt erstellt. Die von der JS-Ebene übergebenen Parameter und Methoden werden in diesem Anforderungsobjekt gekapselt. Die Rückruffunktion, die uns am meisten beschäftigt, wird auf das Attribut oncompete_sym gesetzt. Nachdem das Objekt verpackt wurde, wird das FSReqWrap-Objekt in den Thread-Pool verschoben, um auf die Ausführung zu warten.
An diesem Punkt kehrt der JS-Aufruf sofort zurück und der JS-Thread kann mit der Ausführung nachfolgender Vorgänge fortfahren. Der aktuelle E/A-Vorgang wartet darauf, im Thread-Pool ausgeführt zu werden, wodurch die erste Phase des asynchronen Aufrufs abgeschlossen wird.
2.4 Ausführungsrückruf
Die Rückrufbenachrichtigung ist die zweite Phase der asynchronen E/A. Nachdem die E/A-Operation im Thread-Pool aufgerufen wurde, werden die erhaltenen Ergebnisse gespeichert. Anschließend wird der IOCP benachrichtigt, dass die aktuelle Objektoperation abgeschlossen wurde, und der Thread wird an den Thread-Pool zurückgegeben. Bei jeder Tick-Ausführung ruft der E/A-Beobachter der Ereignisschleife die entsprechende Methode auf, um zu prüfen, ob eine abgeschlossene Anforderung im Thread-Pool vorhanden ist. Wenn dies der Fall ist, wird das Anforderungsobjekt zur E/A-Warteschlange hinzugefügt Beobachter. Dann behandeln Sie es als Ereignis.
3. Asynchrone Nicht-E/A-API
Es gibt auch einige asynchrone APIs in Node, die nichts mit E/A zu tun haben, wie z. B. die Timer setTimeout(), setInterval(), process.nextTick() und setImmdiate(), die Aufgaben sofort asynchron ausführen usw. Hier ist eine kurze Einführung.
3.1 Timer-API
Die browserseitigen APIs von setTimeout() und setInterval() sind konsistent. Ihre Implementierungsprinzipien ähneln denen der asynchronen E/A, sie erfordern jedoch nicht die Beteiligung des E/A-Thread-Pools. Der durch den Aufruf der Timer-API erstellte Timer wird in einen rot-schwarzen Baum innerhalb des Timer-Beobachters eingefügt. Bei jedem Tick der Ereignisschleife wird das Timer-Objekt iterativ aus dem rot-schwarzen Baum entfernt und überprüft, ob der Timer If überschritten hat Wird der Wert überschritten, wird ein Ereignis gebildet und die Callback-Funktion wird sofort ausgeführt. Das Hauptproblem des Timers besteht darin, dass sein Timing nicht besonders präzise ist (Millisekunden, innerhalb der Toleranz).
3.2 API zur sofortigen asynchronen Aufgabenausführung
Bevor Node erschien, riefen viele Leute dies möglicherweise auf, um eine Aufgabe sofort und asynchron auszuführen:
Aufgrund der Eigenschaften der Ereignisschleife ist der Timer nicht genau genug, und die Verwendung eines Timers erfordert die Verwendung eines Rot-Schwarz-Baums, und die zeitliche Komplexität verschiedener Operationen beträgt O(log(n)). Die Methode „process.nextTick()“ stellt die Rückruffunktion nur in die Warteschlange und nimmt sie zur Ausführung in der nächsten Tick-Runde heraus. Die Komplexität ist O(1), was effizienter ist.
Es gibt auch eine setImmediate()-Methode ähnlich der oben genannten Methode, die die Ausführung der Rückruffunktion verzögert. Ersteres hat jedoch eine höhere Priorität als Letzteres, da die Ereignisschleife die Beobachter der Reihe nach überprüft. Darüber hinaus werden die Rückruffunktionen der ersteren in einem Array gespeichert, und jede Tick-Runde führt alle Rückruffunktionen im Array aus. Die Ergebnisse der letzteren werden in einer verknüpften Liste gespeichert, und pro Runde wird nur eine Rückruffunktion ausgeführt Runde Tick.
4. Ereignisgesteuerter und leistungsstarker Server
Im vorherigen Beispiel wurde fs.open() verwendet, um zu erklären, wie Node asynchrone E/A implementiert. Tatsächlich verwendet Node auch asynchrone E/A zur Verarbeitung von Netzwerk-Sockets, was auch die Grundlage für den Aufbau von Webservern durch Node darstellt. Zu den klassischen Servermodellen gehören:
1. Synchron: Es kann jeweils nur eine Anfrage verarbeitet werden, die restlichen Anfragen befinden sich im Wartezustand
2. Pro Prozess/pro Anfrage: Starten Sie einen Prozess für jede Anfrage, aber die Systemressourcen sind begrenzt und nicht skalierbar
3. Pro Thread/pro Anfrage: Starten Sie für jede Anfrage einen Thread. Threads sind leichter als Prozesse, aber jeder Thread belegt eine bestimmte Menge an Speicher. Wenn große gleichzeitige Anforderungen eingehen, wird der Speicher schnell aufgebraucht
Der berühmte Apache verwendet ein Per-Thread/Pro-Request-Format, weshalb es schwierig ist, mit hoher Parallelität umzugehen. Der Knoten verarbeitet Anforderungen ereignisgesteuert, wodurch der Aufwand für das Erstellen und Zerstören von Threads eingespart werden kann. Da das Betriebssystem beim Planen von Aufgaben gleichzeitig über weniger Threads verfügt, sind auch die Kosten für den Kontextwechsel sehr gering. Der Knoten verarbeitet Anfragen auch bei einer großen Anzahl von Verbindungen ordnungsgemäß.
Der bekannte Server Nginx hat ebenfalls die Multithreading-Methode aufgegeben und die gleiche ereignisgesteuerte Methode wie Node übernommen. Heutzutage hat Nginx großes Potenzial, Apache zu ersetzen. Nginx ist in reinem C geschrieben und bietet eine hohe Leistung, eignet sich jedoch nur für Webserver, Reverse-Proxy oder Lastausgleich usw. Node kann die gleichen Funktionen wie Nginx erstellen, auch verschiedene spezifische Geschäfte abwickeln und seine eigene Leistung ist ebenfalls gut. In tatsächlichen Projekten können wir ihre jeweiligen Vorteile kombinieren, um die beste Leistung der Anwendung zu erzielen.