Illustration des berühmten

Parallelität vs Ereignisschleife vs Ereignisschleife + Parallelität

Erläutern wir zunächst die Terminologie.
Parallelität - Bedeutet, dass Sie mehrere Task-Warteschlangen auf mehreren Prozessorkernen / -threads haben. Es ist jedoch völlig anders als bei der parallelen Ausführung, da die parallele Ausführung nicht mehrere Task-Warteschlangen für den parallelen Fall enthält, benötigen wir für die vollständige parallele Ausführung 1 CPU-Kern / Thread pro Task, was in den meisten Fällen nicht definiert werden kann. Aus diesem Grund bedeutet paralleles Programmieren für die moderne Softwareentwicklung manchmal "Nebenläufigkeit". Ich weiß, es ist seltsam, aber es ist offensichtlich das, was wir im Moment haben (es hängt vom CPU- / Thread-Modell des Betriebssystems ab).
Ereignisschleife - Bedeutet einen unendlichen Zyklus mit einem Thread, der jeweils eine Aufgabe ausführt und nicht nur eine einzelne Aufgabenwarteschlange erstellt, sondern auch Aufgaben priorisiert, da bei einer Ereignisschleife nur eine Ressource zur Ausführung (1 Thread) zur Verfügung steht Für einige Aufgaben müssen Sie sofort Prioritäten setzen. In einigen Worten wird dieser Programmieransatz als Thread-sichere Programmierung bezeichnet, da jeweils nur eine Task / Funktion / Operation ausgeführt werden kann. Wenn Sie etwas ändern, wird dies bereits bei der nächsten Taskausführung geändert.

Gleichzeitige Programmierung

In modernen Computern / Servern haben wir mindestens 2 CPU-Kerne und min. 4 CPU-Threads. Aber auf Servern jetzt durchschn. Server haben mindestens 16 CPU-Threads. Wenn Sie also Software schreiben, die etwas Leistung benötigt, sollten Sie auf jeden Fall in Betracht ziehen, diese so zu gestalten, dass alle auf dem Server verfügbaren CPU-Kerne verwendet werden.

Dieses Bild zeigt das Grundmodell der Parallelität, aber es ist nicht so einfach, dass es angezeigt wird :)

Bei einigen gemeinsam genutzten Ressourcen wird das Programmieren von parallelen Zugriffen sehr schwierig. Schauen wir uns beispielsweise diesen einfachen Code für gleichzeitige Zugriffe an.

// Falsche Parallelität mit Go-Sprache
Paket main
importieren (
   "fmt"
   "Zeit"
)
var SharedMap = make (map [string] string)
func changeMap (Wertzeichenfolge) {
    SharedMap ["test"] = Wert
}
func main () {
    go changeMap ("value1")
    go changeMap ("value2")
    time.Sleep (time.Millisecond * 500)
    fmt.Println (SharedMap ["test"])
}
// Dies gibt "value1" oder "value2" aus, die wir nicht genau kennen!

In diesem Fall wird Go 2 gleichzeitige Jobs wahrscheinlich für verschiedene CPU-Kerne auslösen, und wir können nicht vorhersagen, welcher zuerst ausgeführt wird, sodass wir nicht wissen, was am Ende angezeigt wird.
Warum? - Es ist einfach! Wir planen 2 verschiedene Tasks für verschiedene CPU-Kerne, aber sie verwenden eine gemeinsame Variable / einen gemeinsamen Speicher, so dass beide diesen Speicher ändern, und in einigen Fällen wäre dies der Fall eines Programmabsturzes / einer Programmausnahme.

Um die Ausführung der Parallelprogrammierung vorherzusagen, müssen wir einige Sperrfunktionen wie Mutex verwenden. Mit ihm können wir diese gemeinsam genutzte Speicherressource sperren und jeweils nur für eine Aufgabe verfügbar machen.
Diese Art der Programmierung wird als Blockieren bezeichnet, da wir tatsächlich alle Aufgaben blockieren, bis die aktuelle Aufgabe mit dem gemeinsamen Speicher erledigt ist.

Die meisten Entwickler mögen keine gleichzeitige Programmierung, da Parallelität nicht immer Leistung bedeutet. Dies hängt von bestimmten Fällen ab.

Single-Threaded-Ereignisschleife

Dieser Softwareentwicklungsansatz ist viel einfacher als die Parallelprogrammierung. Weil das Prinzip sehr einfach ist. Sie haben jeweils nur eine Taskausführung. In diesem Fall haben Sie kein Problem mit gemeinsam genutzten Variablen / Speicher, da das Programm mit jeweils einer Aufgabe vorhersehbarer ist.

Der allgemeine Ablauf folgt
1. Event Emitter, der der Event Queue eine Aufgabe hinzufügt, die in einem nächsten Schleifenzyklus ausgeführt werden soll
2. Ereignisschleife, die eine Aufgabe aus der Ereigniswarteschlange abruft und sie basierend auf Handlern verarbeitet

Schreiben wir dasselbe Beispiel mit node.js

let SharedMap = {};
const changeMap = (value) => {
    return () => {
        SharedMap ["test"] = Wert
    }
}
// 0 Timeout bedeutet, dass wir eine neue Aufgabe in der Warteschlange für den nächsten Zyklus erstellen
setTimeout (changeMap ("value1"), 0);
setTimeout (changeMap ("value2"), 0);
setTimeout (() => {
   console.log (SharedMap ["test"])
}, 500);
// in diesem Fall gibt Node.js "value2" aus, weil es einfach ist
// Threaded und es hat "nur eine Task-Warteschlange"

Wie Sie sich in diesem Fall vorstellen können, ist der Code vorhersehbarer als bei einem gleichzeitigen Go-Beispiel, und das liegt daran, dass Node.js mit einer JavaScript-Ereignisschleife in einem einzigen Thread-Modus ausgeführt wird.

In einigen Fällen bietet die Ereignisschleife aufgrund des nicht blockierenden Verhaltens eine höhere Leistung als die gleichzeitige Verwendung. Ein sehr gutes Beispiel sind Netzwerkanwendungen, da sie eine einzige Netzwerkverbindungsressource verwenden und Daten nur verarbeiten, wenn sie über Thread-sichere Ereignisschleifen verfügbar sind.

Parallelität + Ereignisschleife - Thread-Pool mit Thread-Sicherheit

Es kann eine große Herausforderung sein, Anwendungen nur gleichzeitig zu erstellen, da es überall Speicherfehler gibt. Andernfalls blockiert Ihre Anwendung Aktionen für jede Aufgabe. Vor allem, wenn Sie maximale Leistung erzielen möchten, müssen Sie beide kombinieren!

Werfen wir einen Blick auf das Thread Pool + Event Loop-Modell von Nginx Web Server Structure

Die Hauptnetzwerk- und Konfigurationsverarbeitung wird aus Sicherheitsgründen von der Worker-Ereignisschleife in einem einzelnen Thread ausgeführt. Wenn Nginx jedoch eine Datei lesen oder HTTP-Anforderungsheader / -Körper verarbeiten muss, die Vorgänge blockieren, sendet es diese Aufgabe an seinen Thread-Pool zur gleichzeitigen Verarbeitung. Und wenn die Aufgabe erledigt ist, wird das Ergebnis an die Ereignisschleife zurückgesendet, um das ausgeführte Ergebnis threadsicher zu verarbeiten.

Wenn Sie diese Struktur verwenden, erhalten Sie sowohl Threadsicherheit als auch Parallelität. Dies ermöglicht es, alle CPU-Kerne für die Leistung zu verwenden und das Prinzip der Nichtblockierung mit einer einzelnen Thread-Ereignisschleife beizubehalten.

Fazit

Eine Menge Software wird mit reiner Parallelität oder mit einer reinen Single-Threaded-Ereignisschleife geschrieben, wobei jedoch beide innerhalb einer einzelnen Anwendung kombiniert werden, um das Schreiben leistungsfähigerer Anwendungen und die Nutzung aller verfügbaren CPU-Ressourcen zu vereinfachen.