Wenn Sie diese Frage stellen müssen, sind Sie wahrscheinlich nicht mit den Funktionen der meisten Webanwendungen / -dienste vertraut. Sie denken wahrscheinlich, dass alle Software dies tun:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
Auf diese Weise funktionieren jedoch keine Webanwendungen oder Anwendungen mit einer Datenbank als Back-End. Web-Apps tun dies:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
In diesem Szenario verbringt die Software den größten Teil ihrer Laufzeit mit 0% CPU-Zeit und wartet auf die Rückkehr der Datenbank.
Multithread-Netzwerk-App:
Multithread-Netzwerk-Apps bewältigen die oben genannte Arbeitslast wie folgt:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
Daher verbringt der Thread die meiste Zeit mit 0% CPU und wartet darauf, dass die Datenbank Daten zurückgibt. Dabei mussten sie den für einen Thread erforderlichen Speicher zuweisen, der einen vollständig separaten Programmstapel für jeden Thread usw. enthält. Außerdem müssten sie einen Thread starten, der zwar nicht so teuer ist wie das Starten eines vollständigen Prozesses, aber immer noch nicht genau billig.
Singlethreaded-Ereignisschleife
Da wir die meiste Zeit mit 0% CPU verbringen, können Sie Code ausführen, wenn wir keine CPU verwenden. Auf diese Weise erhält jede Anforderung immer noch die gleiche CPU-Zeit wie Multithread-Anwendungen, wir müssen jedoch keinen Thread starten. Also machen wir das:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
In der Praxis geben beide Ansätze Daten mit ungefähr der gleichen Latenz zurück, da die Datenbankantwortzeit die Verarbeitung dominiert.
Der Hauptvorteil hier ist, dass wir keinen neuen Thread erzeugen müssen, so dass wir nicht viel Malloc machen müssen, was uns verlangsamen würde.
Magisches, unsichtbares Einfädeln
Das scheinbar Rätselhafte ist, wie beide oben genannten Ansätze es schaffen, die Arbeitslast "parallel" auszuführen. Die Antwort ist, dass die Datenbank mit einem Thread versehen ist. Unsere Single-Threaded-App nutzt also tatsächlich das Multithread-Verhalten eines anderen Prozesses: der Datenbank.
Wo Singlethreaded-Ansatz fehlschlägt
Eine Singlethread-App schlägt fehl, wenn Sie viele CPU-Berechnungen durchführen müssen, bevor Sie die Daten zurückgeben. Nun, ich meine nicht eine for-Schleife, die das Datenbankergebnis verarbeitet. Das ist immer noch meistens O (n). Was ich meine, sind Dinge wie Fourier-Transformation (z. B. MP3-Codierung), Raytracing (3D-Rendering) usw.
Eine weitere Gefahr von Singlethread-Apps besteht darin, dass nur ein einziger CPU-Kern verwendet wird. Wenn Sie also einen Quad-Core-Server haben (heutzutage nicht ungewöhnlich), verwenden Sie die anderen 3 Kerne nicht.
Wo Multithread-Ansatz fehlschlägt
Eine Multithread-App schlägt fehl, wenn Sie pro Thread viel RAM zuweisen müssen. Erstens bedeutet die RAM-Auslastung selbst, dass Sie nicht so viele Anforderungen bearbeiten können wie eine Singlethread-App. Schlimmer noch, Malloc ist langsam. Das Zuweisen vieler, vieler Objekte (wie es bei modernen Webframeworks üblich ist) bedeutet, dass wir möglicherweise langsamer sind als Singlethread-Apps. Hier gewinnt normalerweise node.js.
Ein Anwendungsfall, der Multithreading verschlimmert, ist, wenn Sie eine andere Skriptsprache in Ihrem Thread ausführen müssen. Normalerweise müssen Sie zuerst die gesamte Laufzeit für diese Sprache mallocieren, dann müssen Sie die von Ihrem Skript verwendeten Variablen mallocieren.
Wenn Sie also Netzwerk-Apps in C oder Go oder Java schreiben, ist der Aufwand für das Threading normalerweise nicht allzu hoch. Wenn Sie einen C-Webserver für PHP oder Ruby schreiben, ist es sehr einfach, einen schnelleren Server in Javascript oder Ruby oder Python zu schreiben.
Hybrider Ansatz
Einige Webserver verwenden einen hybriden Ansatz. Nginx und Apache2 implementieren beispielsweise ihren Netzwerkverarbeitungscode als Thread-Pool von Ereignisschleifen. Jeder Thread führt eine Ereignisschleife aus, in der Anforderungen gleichzeitig mit einem Thread verarbeitet werden. Die Anforderungen werden jedoch auf mehrere Threads verteilt.
Einige Single-Threaded-Architekturen verwenden ebenfalls einen Hybridansatz. Anstatt mehrere Threads von einem einzigen Prozess aus zu starten, können Sie mehrere Anwendungen starten, z. B. 4 node.js-Server auf einem Quad-Core-Computer. Anschließend verwenden Sie einen Load Balancer, um die Arbeitslast auf die Prozesse zu verteilen.
Tatsächlich sind die beiden Ansätze technisch identische Spiegelbilder voneinander.