Im traditionellen Programmiermodell ähneln E/A-Vorgänge einem gewöhnlichen lokalen Funktionsaufruf: Das Programm wird blockiert, bevor die Funktion ausgeführt wird, und kann nicht weiter ausgeführt werden. Das Blockieren von E/A stammt aus dem früheren Zeitscheibenmodell. Der Zweck besteht darin, jede Person zu unterscheiden, und jede Person kann normalerweise nur eine Sache gleichzeitig tun Wenn Sie mit dem vorherigen Schritt fertig sind, können Sie entscheiden, was als nächstes zu tun ist. Dieses „Ein Benutzer, ein Prozess“-Modell, das in Computernetzwerken und im Internet weit verbreitet ist, weist jedoch eine schlechte Skalierbarkeit auf. Bei der Verwaltung mehrerer Prozesse wird viel Speicher verbraucht und der Kontextwechsel beansprucht auch viele Ressourcen. Dies stellt eine enorme Belastung für das Betriebssystem dar und mit zunehmender Anzahl von Prozessen nimmt die Systemleistung stark ab.
Multithreading ist ein einfacher Prozess, der den Speicher mit anderen Threads im selben Prozess teilt. Es ähnelt eher einer Erweiterung des herkömmlichen Modells und wird verwendet, um mehrere Threads gleichzeitig auszuführen Beim Warten auf eine E/A-Operation können andere Threads die CPU übernehmen. Wenn die E/A-Operation abgeschlossen ist, wird der zuvor wartende Thread aktiviert. Das heißt, ein laufender Thread kann unterbrochen und später wieder aufgenommen werden. Darüber hinaus können Threads unter manchen Systemen parallel auf verschiedenen Kernen einer Multicore-CPU laufen.
Programmierer wissen nicht, wann der Thread zu einem bestimmten Zeitpunkt ausgeführt wird. Sie müssen beim gleichzeitigen Zugriff auf den gemeinsam genutzten Speicher sehr vorsichtig sein. Daher müssen sie einige Synchronisierungsprimitive verwenden, um den Zugriff auf eine bestimmte Datenstruktur zu synchronisieren Sperren oder Semaphoren Verwenden Sie diese Option, um die Ausführung von Threads in bestimmten Verhaltensweisen und Plänen zu erzwingen. Anwendungen, die stark auf den gemeinsamen Status zwischen Threads angewiesen sind, sind anfällig für seltsame Probleme, die höchst zufällig und schwer zu finden sind.
Eine andere Möglichkeit ist die Verwendung von Multi-Thread-Zusammenarbeit. Sie sind dafür verantwortlich, die CPU explizit freizugeben und die CPU-Zeit für andere Threads bereitzustellen. Es erhöht aber auch die Komplexität des Programms und die Fehlerwahrscheinlichkeit und vermeidet nicht die Probleme des Multithreadings.
Was ist ereignisgesteuerte Programmierung?
Ereignisgesteuerte Programmierung (Evnet-gesteuerte Programmierung) ist ein Programmierstil, bei dem Ereignisse den Ausführungsfluss des Programms bestimmen. Ereignisse werden von Ereignishandlern oder Ereignisrückrufen verarbeitet Ein bestimmtes Ereignis tritt ein, beispielsweise wenn die Datenbank Abfrageergebnisse zurückgibt oder wenn der Benutzer auf eine Schaltfläche klickt.
Denken Sie daran, dass im herkömmlichen Blocking-I/O-Programmiermodell eine Datenbankabfrage wie folgt aussehen könnte:
do_something_with(result);
In einem ereignisgesteuerten Modell würde diese Abfrage so aussehen:
do_something_with(result);
}
query('SELECT * FROM posts WHERE id = 1', query_finished);
Zuerst definieren Sie eine Funktion namens query_finished, die enthält, was nach Abschluss der Abfrage zu tun ist. Übergeben Sie diese Funktion dann als Parameter an die Abfragefunktion. Wenn die Abfrage ausgeführt wird, wird query_finished aufgerufen, anstatt nur die Abfrageergebnisse zurückzugeben.
Wenn das Ereignis eintritt, an dem Sie interessiert sind, wird die von Ihnen definierte Funktion aufgerufen, anstatt einfach den Ergebniswert zurückzugeben. Dieses Programmiermodell wird ereignisgesteuerte Programmierung oder asynchrone Programmierung genannt. Dies ist eine der offensichtlichsten Funktionen von Node. Dieses Programmiermodell bedeutet, dass der aktuelle Prozess beim Ausführen von E/A-Vorgängen nicht blockiert wird Die entsprechende Callback-Funktion wird aufgerufen.
Die unterste Ebene der ereignisgesteuerten Programmierung basiert auf der Ereignisschleife. Die Ereignisschleife ist im Grunde eine Struktur, die kontinuierlich zwei Funktionen aufruft: Ereigniserkennung und Ereignisprozessor-Triggerung. In jeder Schleife muss der Ereignisschleifenmechanismus erkennen, welche Ereignisse aufgetreten sind. Wenn das Ereignis auftritt, findet er die entsprechende Rückruffunktion und ruft sie auf.
Die Ereignisschleife ist lediglich ein Thread, der innerhalb des Prozesses ausgeführt wird. Wenn ein Ereignis auftritt, kann der Ereignishandler alleine ausgeführt werden und wird nicht unterbrochen, das heißt:
1. Zu einem bestimmten Zeitpunkt kann höchstens eine Ereignisrückruffunktion ausgeführt werden
2. Event-Handler werden während der Ausführung von
Damit müssen sich Entwickler nicht mehr um die Thread-Synchronisierung und die gleichzeitige Änderung des gemeinsam genutzten Speichers kümmern.
Ein bekanntes Geheimnis:
Menschen in der Systemprogrammier-Community wissen seit langem, dass ereignisgesteuerte Programmierung der beste Weg ist, Dienste mit hoher Parallelität zu erstellen, da sie nicht viel Kontext speichern muss und daher viel Speicher spart , es gibt nicht so viele Kontextwechsel und es spart viel Ausführungszeit.
Langsam ist dieses Konzept in andere Plattformen und Communities eingedrungen, und es sind einige berühmte Event-Loop-Implementierungen entstanden, wie Rubys Event Machine, Perls AnyEvnet und Pythons Twisted. Darüber hinaus gibt es viele andere Implementierungen und Sprachen.
Um mit diesen Frameworks zu entwickeln, müssen Sie spezifische Kenntnisse in Bezug auf das Framework und die Framework-spezifischen Klassenbibliotheken erlernen. Wenn Sie beispielsweise Event Machine verwenden möchten, müssen Sie die Verwendung vermeiden Synchronisierungsklassenbibliotheken und können nur die asynchrone Klassenbibliothek für Event Machine verwenden. Wenn Sie blockierende Bibliotheken verwenden (wie die meisten Standardbibliotheken von Ruby), verliert Ihr Server die optimale Skalierbarkeit, da die Ereignisschleife weiterhin ständig blockiert wird und gelegentlich die Verarbeitung von E/A-Ereignissen verhindert.
Node wurde ursprünglich als nicht blockierende E/A-Serverplattform konzipiert, daher sollten Sie im Allgemeinen davon ausgehen, dass der gesamte darauf ausgeführte Code nicht blockierend ist. Da JavaScript sehr klein ist und kein I/O-Modell erzwingt (da es keine Standard-I/O-Bibliothek hat), wird Node in einer sehr reinen Umgebung ohne Legacy-Probleme erstellt.
Wie Node und JavaScript asynchrone Anwendungen vereinfachen
Ryan Dahl, der Autor von Node, verwendete zunächst C zur Entwicklung dieses Projekts, stellte jedoch fest, dass die Pflege des Kontexts von Funktionsaufrufen zu kompliziert war, was zu einer hohen Codekomplexität führte. Dann wechselte er zu Lua, aber Lua verfügt bereits über mehrere blockierende und nicht blockierende Bibliotheken, die Entwickler verwirren und viele Menschen daran hindern können, skalierbare Anwendungen zu erstellen, weshalb auch Dahl aufgegeben hat. Schließlich wandte er sich JavaScript zu. Durch Schließungen und Objektfunktionen der ersten Ebene eignete sich JavaScript sehr gut für die ereignisgesteuerte Programmierung. Die Magie von JavaScript ist einer der Hauptgründe, warum Node so beliebt ist.
Was ist ein Abschluss?
Ein Abschluss kann als spezielle Funktion verstanden werden, kann jedoch Variablen in dem Bereich, in dem er definiert ist, erben und darauf zugreifen. Wenn Sie eine Rückruffunktion als Parameter an eine andere Funktion übergeben, wird sie später aufgerufen. Der Zauber besteht darin, dass sie sich beim späteren Aufruf tatsächlich an den Kontext erinnert, in dem sie definiert ist, und an die darin enthaltenen übergeordneten Variablen. und sie sind normal zugänglich. Diese leistungsstarke Funktion ist der Kern des Erfolgs von Node.
Das folgende Beispiel zeigt, wie JavaScript-Abschlüsse in einem Webbrowser funktionieren. Wenn Sie das eigenständige Ereignis einer Schaltfläche abhören möchten, können Sie Folgendes tun:
document.getElementById('myButton').onclick = function() {
clickCount = 1;
warning("clicked " clickCount " times.");
};
So funktioniert es bei der Verwendung von jQuery:
$('button#mybutton').click(function() {
clickedCount ;
warning('Clicked ' clickCount ' times.');
});
In JavaScript sind Funktionen erstklassige Objekte, was bedeutet, dass Sie Funktionen als Parameter an andere Funktionen übergeben können. In den beiden obigen Beispielen weist ersteres eine Funktion einer anderen Funktion zu und letzteres übergibt die Funktion als Parameter an eine andere Funktion. Die Handlerfunktion (Rückruffunktion) des Klickereignisses kann auf jede Variable unter dem Codeblock zugreifen, in dem sich die Funktion befindet definiert ist, hat es in diesem Fall Zugriff auf die ClickCount-Variable, die in seinem übergeordneten Abschluss definiert ist.
Die Variable clickCount befindet sich im globalen Bereich (dem äußersten Bereich in JavaScript). Sie speichert die Anzahl der Klicks des Benutzers auf die Schaltfläche. Es ist normalerweise eine schlechte Angewohnheit, Variablen im globalen Bereich zu speichern, da es leicht zu Konflikten kommen kann Bei anderem Code sollten Sie Variablen im lokalen Bereich platzieren, in dem sie verwendet werden. Meistens ist das bloße Einschließen des Codes in eine Funktion gleichbedeutend mit dem Erstellen eines zusätzlichen Abschlusses, sodass Sie eine Verschmutzung der globalen Umgebung leicht vermeiden können, wie folgt:
var clickCount = 0;
$('button#mybutton').click(function() {
clickCount ;
warning('Clicked ' clickCount ' times.');
});
}());
Hinweis: Die siebte Zeile des obigen Codes definiert eine Funktion und ruft sie sofort auf. Dies ist ein häufiges Entwurfsmuster in JavaScript: Erstellen eines neuen Bereichs durch Erstellen einer Funktion.
Wie Abschlüsse die asynchrone Programmierung unterstützen
Im ereignisgesteuerten Programmiermodell schreiben Sie zunächst den Code, der nach dem Eintreten des Ereignisses ausgeführt wird, fügen Sie dann den Code in eine Funktion ein und übergeben Sie die Funktion schließlich als Parameter an den Aufrufer, der später aufgerufen wird die Anruferfunktion.In JavaScript ist eine Funktion keine isolierte Definition. Sie merkt sich auch den Kontext des Bereichs, in dem sie deklariert ist. Dieser Mechanismus ermöglicht es JavaScript-Funktionen, auf den Kontext zuzugreifen, in dem die Funktion definiert ist, und auf den übergeordneten Kontext .
Wenn Sie eine Rückruffunktion als Parameter an den Aufrufer übergeben, wird diese Funktion zu einem späteren Zeitpunkt aufgerufen. Auch wenn der Bereich, in dem die Rückruffunktion definiert ist, beendet ist, kann die Rückruffunktion beim Aufruf immer noch auf alle Variablen im beendeten Bereich und seinem übergeordneten Bereich zugreifen. Wie im letzten Beispiel wird die Callback-Funktion innerhalb von jQuerys click() aufgerufen, kann aber weiterhin auf die Variable clickCount zugreifen.
Die Magie von Abschlüssen wurde bereits gezeigt. Durch die Übergabe von Zustandsvariablen an eine Funktion können Sie ereignisgesteuert programmieren, ohne den Abschlussmechanismus von JavaScript beizubehalten.
Zusammenfassung
Ereignisgesteuerte Programmierung ist ein Programmiermodell, das den Programmausführungsfluss durch Ereignisauslösung bestimmt. Programmierer registrieren Rückruffunktionen (oft als Event-Handler bezeichnet) für die Ereignisse, an denen sie interessiert sind, und das System ruft dann den registrierten Event-Handler auf, wenn das Ereignis auftritt. Dieses Programmiermodell bietet viele Vorteile, die das herkömmliche blockierende Programmiermodell nicht bietet. Um ähnliche Funktionen zu erreichen, musste man in der Vergangenheit Multiprozess/Multithreading verwenden.JavaScript ist eine leistungsstarke Sprache, da sie sich aufgrund ihrer Funktion und Abschlusseigenschaften von Objekten ersten Typs gut für die ereignisgesteuerte Programmierung eignet.