Ich habe gerade das folgende Video gesehen: Einführung in Node.js und verstehe immer noch nicht, wie Sie die Geschwindigkeitsvorteile erhalten.
Hauptsächlich sagt Ryan Dahl (der Ersteller von Node.js), dass Node.js auf Ereignisschleifen anstatt auf Threads basiert. Threads sind teuer und sollten nur den Experten der gleichzeitigen Programmierung überlassen werden.
Später zeigt er dann den Architekturstapel von Node.js mit einer zugrunde liegenden C-Implementierung, die intern über einen eigenen Thread-Pool verfügt. Offensichtlich würden Node.js Entwickler niemals ihre eigenen Threads starten oder den Thread-Pool direkt verwenden ... sie verwenden asynchrone Rückrufe. So viel verstehe ich.
Was ich nicht verstehe, ist der Punkt, dass Node.js immer noch Threads verwendet ... es verbirgt nur die Implementierung. Wie ist das schneller, wenn 50 Leute 50 Dateien anfordern (derzeit nicht im Speicher), dann sind nicht 50 Threads erforderlich ?
Der einzige Unterschied besteht darin, dass der Entwickler von Node.js, da es intern verwaltet wird, die Thread-Details nicht codieren muss, sondern weiterhin die Threads verwendet, um die Anforderungen für E / A-Dateien (Blockierungsdateien) zu verarbeiten.
Nehmen Sie also nicht wirklich nur ein Problem (Threading) und verstecken es, solange dieses Problem noch besteht: hauptsächlich mehrere Threads, Kontextwechsel, Deadlocks ... usw.?
Es muss einige Details geben, die ich hier immer noch nicht verstehe.
quelle
select()
ist schneller als das Austauschen von Thread-Kontexten.Antworten:
Es gibt tatsächlich ein paar verschiedene Dinge, die hier zusammengeführt werden. Aber es beginnt mit dem Mem, dass Threads einfach sehr hart sind. Wenn sie also hart sind, ist es wahrscheinlicher, dass Sie Threads verwenden, um 1) aufgrund von Fehlern zu brechen und 2) sie nicht so effizient wie möglich zu verwenden. (2) ist derjenige, nach dem Sie fragen.
Denken Sie an eines der Beispiele, die er gibt, bei denen eine Anfrage eingeht und Sie eine Abfrage ausführen, und machen Sie dann etwas mit den Ergebnissen davon. Wenn Sie es in einer Standardprozedur schreiben, sieht der Code möglicherweise folgendermaßen aus:
Wenn Sie aufgrund der eingehenden Anforderung einen neuen Thread erstellt haben, in dem der obige Code ausgeführt wurde, befindet sich dort ein Thread, der während der
query()
Ausführung überhaupt nichts unternimmt . (Laut Ryan verwendet Apache einen einzelnen Thread, um die ursprüngliche Anforderung zu erfüllen, während Nginx ihn in den Fällen, über die er spricht, übertrifft, weil dies nicht der Fall ist.)Wenn Sie wirklich schlau wären, würden Sie den obigen Code so ausdrücken, dass die Umgebung während der Ausführung der Abfrage etwas anderes tun könnte:
Dies ist im Grunde das, was node.js tut. Sie dekorieren Ihren Code im Grunde genommen so, dass die Umgebung klug darüber ist, was wann ausgeführt wird - auf eine Weise, die aufgrund der Sprache und der Umgebung, daher die Punkte bei Schließungen, praktisch ist. Auf diese Weise ist node.js nicht neu in dem Sinne, dass es asynchrone E / A erfunden hat (nicht dass irgendjemand so etwas behauptet hat), aber es ist neu darin, dass die Art und Weise, wie es ausgedrückt wird, etwas anders ist.
Hinweis: Wenn ich sage, dass die Umgebung klug sein kann, was wann ausgeführt wird, meine ich insbesondere, dass der Thread, mit dem eine E / A gestartet wurde, jetzt zur Verarbeitung einer anderen Anforderung oder einer möglichen Berechnung verwendet werden kann parallel oder starten Sie eine andere parallele E / A. (Ich bin nicht sicher, ob der Knoten hoch genug ist, um mehr Arbeit für dieselbe Anfrage zu starten, aber Sie haben die Idee.)
quelle
Hinweis! Dies ist eine alte Antwort. Während es in der groben Gliederung immer noch zutrifft, haben sich einige Details möglicherweise aufgrund der rasanten Entwicklung von Node in den letzten Jahren geändert.
Es werden Threads verwendet, weil:
Um nicht blockierende E / A zu fälschen, sind Threads erforderlich: Blockieren Sie E / A in einem separaten Thread. Es ist eine hässliche Lösung und verursacht viel Aufwand.
Auf Hardware-Ebene ist es noch schlimmer:
Das ist einfach nur dumm und ineffizient. Aber es funktioniert zumindest! Wir können Node.js genießen, weil es die hässlichen und umständlichen Details hinter einer ereignisgesteuerten asynchronen Architektur verbirgt.
Vielleicht wird jemand in Zukunft O_NONBLOCK für Dateien implementieren? ...
Bearbeiten: Ich habe dies mit einem Freund besprochen und er sagte mir, dass eine Alternative zu Threads das Abrufen mit select ist : Geben Sie ein Timeout von 0 an und führen Sie E / A für die zurückgegebenen Dateideskriptoren durch (jetzt, da sie garantiert nicht blockieren).
quelle
Ich fürchte, ich mache hier "das Falsche", wenn ja, lösche mich und ich entschuldige mich. Insbesondere sehe ich nicht, wie ich die hübschen kleinen Anmerkungen erstelle, die einige Leute erstellt haben. Ich habe jedoch viele Bedenken / Beobachtungen zu diesem Thema.
1) Das kommentierte Element im Pseudocode in einer der populären Antworten
ist im Wesentlichen falsch. Wenn der Thread rechnet, dreht er nicht mit den Daumen, sondern erledigt die notwendige Arbeit. Wenn andererseits nur auf den Abschluss der E / A gewartet wird und dann keine CPU-Zeit verwendet wird, besteht der springende Punkt der Thread-Steuerungsinfrastruktur im Kernel darin, dass die CPU etwas Nützliches findet. Die einzige Möglichkeit, "Daumen zu drehen", wie hier vorgeschlagen, besteht darin, eine Abfrageschleife zu erstellen, und niemand, der einen echten Webserver codiert hat, ist dazu nicht in der Lage.
2) "Threads sind hart", macht nur im Zusammenhang mit dem Datenaustausch Sinn. Wenn Sie im Wesentlichen unabhängige Threads haben, wie dies bei der Verarbeitung unabhängiger Webanforderungen der Fall ist, ist das Threading trivial einfach. Sie codieren einfach den linearen Ablauf für die Verarbeitung eines Jobs und wissen, dass mehrere Anforderungen verarbeitet werden wird effektiv unabhängig sein. Persönlich würde ich es wagen, dass für die meisten Programmierer das Erlernen des Schließ- / Rückrufmechanismus komplexer ist als das einfache Codieren der Top-to-Bottom-Thread-Version. (Aber ja, wenn Sie zwischen den Threads kommunizieren müssen, wird das Leben sehr schnell sehr schwer, aber dann bin ich nicht davon überzeugt, dass der Schließ- / Rückrufmechanismus dies wirklich ändert. Er schränkt nur Ihre Optionen ein, da dieser Ansatz mit Threads immer noch erreichbar ist Wie auch immer, das '
3) Bisher hat niemand echte Beweise dafür vorgelegt, warum eine bestimmte Art von Kontextwechsel mehr oder weniger zeitaufwändig wäre als jede andere Art. Meine Erfahrung bei der Erstellung von Multitasking-Kerneln (im kleinen Maßstab für eingebettete Controller nichts Besonderes als ein "echtes" Betriebssystem) legt nahe, dass dies nicht der Fall wäre.
4) Alle Abbildungen, die ich bisher gesehen habe, um zu zeigen, wie viel schneller Node ist als andere Webserver, sind schrecklich fehlerhaft. Sie sind jedoch auf eine Weise fehlerhaft, die indirekt einen Vorteil veranschaulicht, den ich definitiv für Node akzeptieren würde (und es ist keineswegs unbedeutend). Der Knoten sieht nicht so aus, als müsste er optimiert werden (und erlaubt es auch nicht). Wenn Sie ein Thread-Modell haben, müssen Sie genügend Threads erstellen, um die erwartete Last zu bewältigen. Wenn Sie dies schlecht machen, erhalten Sie eine schlechte Leistung. Wenn zu wenige Threads vorhanden sind, befindet sich die CPU im Leerlauf, kann jedoch keine weiteren Anforderungen akzeptieren, erstellt zu viele Threads, und Sie verschwenden Kernelspeicher. In einer Java-Umgebung verschwenden Sie auch den Hauptspeicher des Heapspeichers . Für Java ist die Verschwendung von Heap der erste und beste Weg, um die Leistung des Systems zu verbessern. Weil eine effiziente Speicherbereinigung (derzeit könnte sich dies mit G1 ändern, aber es scheint, dass die Jury zumindest Anfang 2013 noch nicht über diesen Punkt informiert ist) davon abhängt, dass viel Ersatzhaufen vorhanden ist. Es gibt also das Problem: Optimieren Sie es mit zu wenigen Threads, Sie haben inaktive CPUs und einen schlechten Durchsatz, optimieren Sie es mit zu vielen und es bleibt auf andere Weise hängen.
5) Es gibt eine andere Art und Weise, wie ich die Logik der Behauptung akzeptiere, dass der Ansatz von Node "von Natur aus schneller ist", und das ist dies. Die meisten Thread-Modelle verwenden ein zeitlich begrenztes Kontextwechselmodell, das über dem geeigneteren (Wertbeurteilungsalarm :) und effizienteren (kein Wertbeurteilungs-) Präventivmodell liegt. Dies geschieht aus zwei Gründen: Erstens scheinen die meisten Programmierer die Prioritätsvoraussetzung nicht zu verstehen, und zweitens, wenn Sie das Threading in einer Windows-Umgebung lernen, ist das Timeslicing vorhanden, ob Sie es mögen oder nicht (dies verstärkt natürlich den ersten Punkt Insbesondere in den ersten Versionen von Java wurde bei Solaris-Implementierungen die Prioritätsvoraussetzung und bei Windows die Zeiteinteilung verwendet, da die meisten Programmierer nicht verstanden und sich darüber beschwert haben, dass "Threading in Solaris nicht funktioniert". Sie haben das Modell überall auf Zeitscheibe geändert. Unter dem Strich führt Timeslicing zu zusätzlichen (und möglicherweise unnötigen) Kontextwechseln. Jeder Kontextwechsel benötigt CPU-Zeit, und diese Zeit wird effektiv von der Arbeit entfernt, die für den eigentlichen Job ausgeführt werden kann. Die Zeit, die aufgrund von Zeitscheiben in den Kontextwechsel investiert wird, sollte jedoch nicht mehr als einen sehr kleinen Prozentsatz der Gesamtzeit betragen, es sei denn, es passiert etwas ziemlich Außergewöhnliches, und es gibt keinen Grund, warum ich davon ausgehen kann, dass dies in einem Fall der Fall ist einfacher Webserver). Ja, die überschüssigen Kontextwechsel, die mit Timeslicing verbunden sind, sind ineffizient (und diese treten in nicht auf und diese Zeit wird effektiv von der Arbeit entfernt, die an der eigentlichen Arbeit erledigt werden kann. Die Zeit, die aufgrund von Zeitscheiben in den Kontextwechsel investiert wird, sollte jedoch nicht mehr als einen sehr kleinen Prozentsatz der Gesamtzeit betragen, es sei denn, es passiert etwas ziemlich Außergewöhnliches, und es gibt keinen Grund, warum ich davon ausgehen kann, dass dies in einem Fall der Fall ist einfacher Webserver). Ja, die überschüssigen Kontextwechsel, die mit Timeslicing verbunden sind, sind ineffizient (und diese treten in nicht auf und diese Zeit wird effektiv von der Arbeit entfernt, die an der eigentlichen Arbeit erledigt werden kann. Die Zeit, die aufgrund von Zeitscheiben in den Kontextwechsel investiert wird, sollte jedoch nicht mehr als einen sehr kleinen Prozentsatz der Gesamtzeit betragen, es sei denn, es passiert etwas ziemlich Außergewöhnliches, und es gibt keinen Grund, warum ich davon ausgehen kann, dass dies in einem Fall der Fall ist einfacher Webserver). Ja, die überschüssigen Kontextwechsel, die mit Timeslicing verbunden sind, sind ineffizient (und diese treten in nicht aufKernel- Threads in der Regel übrigens), aber der Unterschied beträgt einige Prozent des Durchsatzes, nicht die Art von Ganzzahlfaktoren, die in den Leistungsansprüchen enthalten sind, die häufig für Node impliziert werden.
Wie auch immer, ich entschuldige mich dafür, dass alles lang und unruhig ist, aber ich habe wirklich das Gefühl, dass die Diskussion bisher nichts bewiesen hat, und ich würde mich freuen, von jemandem in einer dieser beiden Situationen zu hören:
a) eine echte Erklärung, warum Node besser sein sollte (über die beiden oben beschriebenen Szenarien hinaus, von denen das erste (schlechte Abstimmung) meiner Meinung nach die wahre Erklärung für alle Tests ist, die ich bisher gesehen habe ] Je mehr ich darüber nachdenke, desto mehr frage ich mich, ob der von einer großen Anzahl von Stapeln verwendete Speicher hier von Bedeutung sein könnte. Die Standardstapelgrößen für moderne Threads sind in der Regel ziemlich groß, aber der von a zugewiesene Speicher Ein geschlossenes Ereignissystem wäre nur das, was benötigt wird.
b) ein echter Benchmark, der dem Thread-Server Ihrer Wahl tatsächlich eine faire Chance bietet. Zumindest auf diese Weise müsste ich aufhören zu glauben, dass die Behauptungen im Wesentlichen falsch sind;> ([Bearbeiten] das ist wahrscheinlich eher stärker als beabsichtigt, aber ich bin der Meinung, dass die Erklärungen für Leistungsvorteile bestenfalls unvollständig sind, und die Die gezeigten Benchmarks sind nicht zumutbar.
Prost, Toby
quelle
open()
kann POSIX nicht blockierungsfrei gemacht werden?). Auf diese Weise werden alle Leistungseinbußen amortisiert, bei denen das herkömmlichefork()
/pthread_create()
-on-Request-Modell Threads erstellen und zerstören müsste. Und wie in Postscript a) erwähnt, amortisiert dies auch das Problem mit dem Stapelspeicher. Sie können wahrscheinlich Tausende von Anfragen mit beispielsweise 16 E / A-Threads bearbeiten.Ryan verwendet Threads für die Teile, die blockieren (die meisten node.js verwenden nicht blockierende E / A), da einige Teile wahnsinnig schwer zu schreiben sind und nicht blockieren. Aber ich glaube, Ryan möchte, dass alles nicht blockiert. Auf Folie 63 (internes Design) sehen Sie, dass Ryan libev (Bibliothek, die asynchrone Ereignisbenachrichtigungen abstrahiert) für die nicht blockierende Ereignisschleife verwendet . Aufgrund der Ereignisschleife benötigt node.js weniger Threads, was die Kontextumschaltung, den Speicherverbrauch usw. reduziert.
quelle
Threads werden nur verwendet, um Funktionen zu behandeln, die keine asynchrone Funktion haben, wie z
stat()
.Die
stat()
Funktion blockiert immer, daher muss node.js einen Thread verwenden, um den eigentlichen Aufruf auszuführen, ohne den Hauptthread zu blockieren (Ereignisschleife). Möglicherweise wird niemals ein Thread aus dem Thread-Pool verwendet, wenn Sie diese Art von Funktionen nicht aufrufen müssen.quelle
Ich weiß nichts über die internen Abläufe von node.js, aber ich kann sehen, wie die Verwendung einer Ereignisschleife die Behandlung von Thread-E / A übertreffen kann. Stellen Sie sich eine Disc-Anfrage vor, geben Sie mir staticFile.x und stellen Sie 100 Anfragen für diese Datei. Jede Anforderung belegt normalerweise einen Thread, der diese Datei erneut empfängt, dh 100 Threads.
Stellen Sie sich nun die erste Anforderung vor, die einen Thread erstellt, der zu einem Herausgeberobjekt wird. Alle 99 anderen Anforderungen prüfen zunächst, ob ein Herausgeberobjekt für staticFile.x vorhanden ist. Wenn ja, hören Sie es sich an, während es seine Arbeit erledigt. Andernfalls starten Sie einen neuen Thread und damit einen neues Publisher-Objekt.
Sobald der einzelne Thread fertig ist, übergibt er staticFile.x an alle 100 Listener und zerstört sich selbst, sodass bei der nächsten Anforderung ein neuer Thread und ein neues Publisher-Objekt erstellt werden.
Es sind also 100 Threads gegenüber 1 Thread im obigen Beispiel, aber auch 1 Disc-Lookup anstelle von 100 Disc-Lookups. Die Verstärkung kann durchaus phänomenal sein. Ryan ist ein kluger Kerl!
Eine andere Sichtweise ist eines seiner Beispiele zu Beginn des Films. Anstatt:
Wieder 100 separate Abfragen an eine Datenbank im Vergleich zu ...:
Wenn eine Abfrage bereits ausgeführt wurde, sprangen andere gleichwertige Abfragen einfach auf den Zug, sodass Sie 100 Abfragen in einer einzigen Datenbankrundfahrt haben können.
quelle