Angenommen, Sie gehen in ein Restaurant und es gibt einen einzigen Koch, der verspricht: „Ich kann für Hunderte von Menschen gleichzeitig kochen und keiner von Ihnen wird hungrig sein.“ Klingt unmöglich, oder? Sie können diese einzelne Prüfung als Node JS betrachten, der all diese Mehrfachbestellungen verwaltet und dennoch das Essen an alle Kunden serviert.
Wenn Sie jemandem die Frage „Was ist Node JS?“ stellen, erhält eine Person immer die Antwort „Node JS ist eine Laufzeitumgebung, die zum Ausführen von JavaScript außerhalb der Browserumgebung verwendet wird.“
Aber was bedeutet Laufzeit? ... Die Laufzeitumgebung ist eine Software-Infrastruktur, in der die Codeausführung in eine bestimmte Programmiersprache geschrieben wird. Es verfügt über alle Tools, Bibliotheken und Funktionen zum Ausführen von Code, zum Behandeln von Fehlern, zum Verwalten des Speichers und kann mit dem zugrunde liegenden Betriebssystem oder der zugrunde liegenden Hardware interagieren.
Node JS verfügt über all dies.
Google V8 Engine zum Ausführen des Codes.
Kernbibliotheken und APIs wie fs, crypto, http usw.
Infrastruktur wie Libuv und die Event Loop zur Unterstützung asynchroner und nicht blockierender E/A-Vorgänge.
So können wir jetzt wissen, warum Node JS Runtime heißt.
Diese Laufzeit besteht aus zwei unabhängigen Abhängigkeiten, V8 und libuv.
V8 ist eine Engine, die auch in Google Chrome verwendet wird und von Google entwickelt und verwaltet wird. In Node JS führt es den JavaScript-Code aus. Wenn wir den Befehl node index.js ausführen, übergibt Node JS diesen Code an die V8-Engine. V8 verarbeitet diesen Code, führt ihn aus und stellt das Ergebnis bereit. Wenn Ihr Code beispielsweise „Hello, World!“ protokolliert. Zur Konsole übernimmt V8 die eigentliche Ausführung, die dies ermöglicht.
Die libuv-Bibliothek enthält den C-Code, der den Zugriff auf das Betriebssystem ermöglicht, wenn wir Funktionen wie Netzwerk, E/A-Vorgänge oder zeitbezogene Vorgänge benötigen. Es fungiert als Brücke zwischen Node JS und dem Betriebssystem.
Die libuv übernimmt die folgenden Vorgänge:
Dateisystemoperationen: Dateien lesen oder schreiben (fs.readFile, fs.writeFile).
Netzwerk: Verarbeiten von HTTP-Anfragen, Sockets oder Herstellen einer Verbindung zu Servern.
Timer: Verwalten von Funktionen wie setTimeout oder setInterval.
Aufgaben wie das Lesen von Dateien werden vom Libuv-Thread-Pool, Timer vom Libuv-Timersystem und Netzwerkaufrufe von APIs auf Betriebssystemebene übernommen.
Sehen Sie sich das folgende Beispiel an.
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
Wir lesen dieselbe Datei viermal und protokollieren die Zeit zum Lesen dieser Dateien.
Wir erhalten die folgende Ausgabe dieses Codes.
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
Wir können sehen, dass wir alle vier Dateien fast nach 50 ms gelesen haben. Wenn Node JS Single-Threaded ist, wie werden dann alle Lesevorgänge dieser Dateien gleichzeitig abgeschlossen?
Diese Frage beantwortet, dass die libuv-Bibliothek den Thread-Pool verwendet. Der Thread-Pool besteht aus einer Reihe von Threads. Standardmäßig beträgt die Thread-Pool-Größe 4, was bedeutet, dass 4 Anfragen gleichzeitig von libuv verarbeitet werden können.
Stellen Sie sich ein anderes Szenario vor, in dem wir statt einer Datei viermal diese Datei sechsmal lesen.
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
Die Ausgabe sieht folgendermaßen aus:
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
Angenommen, die Lesevorgänge 1 und 2 sind abgeschlossen und Thread 1 und 2 werden frei.
Sie können sehen, dass wir bei den ersten vier Malen fast die gleiche Zeit zum Lesen der Datei haben, aber wenn wir diese Datei beim fünften und sechsten Mal lesen, dauert es fast doppelt so lange, bis die Lesevorgänge abgeschlossen sind, als bei den ersten vier Lesevorgängen .
Dies geschieht, weil die Thread-Pool-Größe standardmäßig 4 beträgt, sodass vier Lesevorgänge gleichzeitig ausgeführt werden. Beim zweiten (5. und 6.) Lesen der Datei wartet libuv dann jedoch, weil alle Threads Arbeit haben. Wenn einer der vier Threads die Ausführung abschließt, wird der fünfte Lesevorgang für diesen Thread ausgeführt und das Gleiche wird für den sechsten Lesevorgang durchgeführt. Das ist der Grund, warum es länger dauert.
Node JS ist also kein Single-Threaded.
Aber warum bezeichnen manche Leute es als Single-Threaded?
Das liegt daran, dass die Hauptereignisschleife Single-Threaded ist. Dieser Thread ist für die Ausführung des Node-JS-Codes verantwortlich, einschließlich der Verarbeitung asynchroner Rückrufe und der Koordinierung von Aufgaben. Blockierende Vorgänge wie Datei-E/A werden nicht direkt verarbeitet.
Der Ablauf der Codeausführung sieht so aus.
Node.js führt den gesamten synchronen (blockierenden) Code Zeile für Zeile mit der V8-JavaScript-Engine aus.
Asynchrone Vorgänge wie fs.readFile, setTimeout oder http-Anfragen werden an die Libuv-Bibliothek oder andere Subsysteme (z. B. Betriebssystem) gesendet.
Aufgaben wie das Lesen von Dateien werden vom Libuv-Thread-Pool, Timer vom Libuv-Timersystem und Netzwerkaufrufe von APIs auf Betriebssystemebene übernommen.
Sobald eine asynchrone Aufgabe abgeschlossen ist, wird der zugehörige Rückruf an die Warteschlange der Ereignisschleife gesendet.
Die Ereignisschleife nimmt Rückrufe aus der Warteschlange auf und führt sie einzeln aus, um eine nicht blockierende Ausführung sicherzustellen.
Sie können die Thread-Pool-Größe mit process.env.UV_THREADPOOL_SIZE = 8 ändern.
Jetzt denke ich, dass wir auch die große Anzahl an Anfragen bewältigen können, wenn wir die Anzahl der Threads hoch festlegen. Ich hoffe, dass Sie darüber genauso denken wie ich.
Aber es ist das Gegenteil von dem, was wir dachten.
Wenn wir die Anzahl der Threads über einen bestimmten Grenzwert hinaus erhöhen, verlangsamt dies die Ausführung Ihres Codes.
Sehen Sie sich das folgende Beispiel an.
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 4; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
Ausgabe:
Mit hoher Thread-Pool-Größe (100 Threads)
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
Die folgende Ausgabe erfolgt nun, wenn wir die Thread-Pool-Größe auf 4 (Standardgröße) festlegen.
Mit Standard-Thread-Pool-Größe (4 Threads)
const fs = require('fs'); const path = require('path'); const filePath = path.join(__dirname, 'file.txt'); const readFileWithTiming = (index) => { const start = Date.now(); fs.readFile(filePath, 'utf8', (err, data) => { if (err) { console.error(`Error reading the file for task ${index}:`, err); return; } const end = Date.now(); console.log(`Task ${index} completed in ${end - start}ms`); }); }; const startOverall = Date.now(); for (let i = 1; i <= 6; i++) { readFileWithTiming(i); } process.on('exit', () => { const endOverall = Date.now(); console.log(`Total execution time: ${endOverall - startOverall}ms`); });
Sie können sehen, dass die Gesamtausführungszeit einen Unterschied von 100 ms aufweist. Die Gesamtausführungszeit (Thread-Pool-Größe 4) beträgt 600 ms und die Gesamtausführungszeit (Thread-Pool-Größe 100) beträgt 700 ms. Daher nimmt eine Thread-Pool-Größe von 4 weniger Zeit in Anspruch.
Warum die hohe Anzahl an Threads! = mehr Aufgaben können gleichzeitig bearbeitet werden?
Der erste Grund ist, dass jeder Thread seinen eigenen Stapel- und Ressourcenbedarf hat. Wenn Sie die Anzahl der Threads erhöhen, führt dies letztendlich dazu, dass nicht genügend Arbeitsspeicher oder CPU-Ressourcen vorhanden sind.
Der zweite Grund ist, dass Betriebssysteme Threads planen müssen. Wenn es zu viele Threads gibt, verbringt das Betriebssystem viel Zeit damit, zwischen ihnen zu wechseln (Kontextwechsel), was den Overhead erhöht und die Leistung verlangsamt, anstatt sie zu verbessern.
Jetzt können wir sagen, dass es nicht darum geht, die Thread-Pool-Größe zu erhöhen, um Skalierbarkeit und hohe Leistung zu erreichen, sondern dass es darum geht, die richtige Architektur, wie z. B. Clustering, zu verwenden und die Art der Aufgabe zu verstehen (E/A vs. CPU-gebunden). ) und wie das ereignisgesteuerte Modell von Node.js funktioniert.
Danke fürs Lesen.
Das obige ist der detaillierte Inhalt vonKnoten-JS-Interna. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!