假設你去一家餐廳,有一位廚師承諾“我可以同時為數百人做飯,而你們不會挨餓”,聽起來不可能,對吧?您可以將這個單一檢查視為 Node JS,它管理所有這些多個訂單,並且仍然為所有顧客提供食物。
每當你問某人「什麼是 Node JS?」時,人們總是得到答案「Node JS 是一個運行時,用於在瀏覽器環境之外運行 JavaScript」。
但是,運行時是什麼意思? ……運行時環境是一種軟體基礎設施,其中程式碼執行被編寫成特定的程式語言。它擁有運行程式碼、處理錯誤、管理記憶體以及與底層作業系統或硬體互動的所有工具、函式庫和功能。
Node JS 擁有所有這些。
Google V8 引擎來運作程式碼。
fs、crypto、http 等核心函式庫與 API
Libuv 和事件循環等基礎設施支援非同步和非阻塞 I/O 操作。
所以,我們現在可以知道為什麼 Node JS 稱為運行時了。
此運行時由兩個獨立的依賴項組成,V8 和 libuv.
V8 是 Google Chrome 中也使用的引擎,由 Google 開發和管理。在 Node JS 中,它執行 JavaScript 程式碼。當我們執行指令 node index.js 時,Node JS 會將此程式碼傳遞給 V8 引擎。 V8 處理該程式碼、執行它並提供結果。例如,如果您的程式碼記錄「Hello, World!」對於控制台,V8 處理實現此操作的實際執行。
libuv 函式庫包含 C 程式碼,當我們需要網路、I/O 操作或與時間相關的操作等功能時,可以使用該程式碼存取作業系統。它可作為 Node JS 和作業系統之間的橋樑。
libuv 處理以下操作:
檔案系統操作:讀取或寫入檔案(fs.readFile、fs.writeFile)。
網路:處理 HTTP 請求、套接字或連接到伺服器。
計時器:管理 setTimeout 或 setInterval 等函數。
檔案讀取等任務由 Libuv 執行緒池處理,計時器由 Libuv 的計時器系統處理,網路呼叫由作業系統級 API 處理。
看下面的例子。
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`); });
我們正在讀取同一個檔案四次,我們正在記錄讀取這些檔案的時間。
我們得到此程式碼的以下輸出。
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
我們可以看到我們幾乎在第 50 毫秒完成了所有四個檔案的讀取。如果 Node JS 是單執行緒的話,那麼這些檔案讀取操作是如何同時完成的呢?
這個問題回答了libuv函式庫使用執行緒池。線程池是一堆線程。預設情況下,執行緒池大小為 4,表示 libuv 可以一次處理 4 個請求。
考慮另一種情況,我們不是讀取一個檔案 4 次,而是讀取該檔案 6 次。
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`); });
輸出將如下圖所示:
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
假設讀取操作1和2完成且執行緒1和2空閒。
您可以看到,前4 次我們讀取文件的時間幾乎相同,但是當我們第5 次和第6 次讀取該文件時,完成讀取操作所需的時間幾乎是前4 次讀取操作的兩倍。
發生這種情況是因為線程池大小預設為4,因此同時處理四個讀取操作,但我們再次讀取檔案2 次(第5 次和第6 次),然後libuv 等待,因為所有線程都有一些工作。當四個執行緒之一完成執行時,將對該執行緒處理第 5 次讀取操作,並且將執行第 6 次讀取操作。這就是為什麼需要更多時間的原因。
所以,Node JS 不是單線程的。
但是,為什麼有些人稱之為單線程?
這是因為主事件循環是單執行緒的。此執行緒負責執行 Node JS 程式碼,包括處理非同步回呼和協調任務。它不直接處理檔案 I/O 等阻塞操作。
程式碼執行流程是這樣的。
Node.js 使用 V8 JavaScript 引擎逐行執行所有同步(阻塞)程式碼。
諸如 fs.readFile、setTimeout 或 http 請求之類的非同步操作被傳送到 Libuv 函式庫或其他子系統(例如作業系統)。
檔案讀取等任務由 Libuv 執行緒池處理,計時器由 Libuv 的計時器系統處理,網路呼叫由作業系統級 API 處理。
非同步任務完成後,其關聯的回呼將被傳送到事件循環的佇列。
事件循環從佇列中取得回呼並一一執行它們,確保非阻塞執行。
您可以使用 process.env.UV_THREADPOOL_SIZE = 8.
來變更執行緒池大小現在,我在想,如果我們設定大量的線程,那麼我們也將能夠處理大量的請求。我希望你能像我一樣思考這個問題。
但是,這與我們的想法相反。
如果我們增加執行緒數量超過一定限制,那麼它會減慢你的程式碼執行速度。
看下面的例子。
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`); });
輸出:
具有高執行緒池大小(100 個執行緒)
Task 1 completed in 50ms Task 2 completed in 51ms Task 3 completed in 52ms Task 4 completed in 53ms Total execution time: 54ms
現在,以下輸出是當我們將執行緒池大小設為 4(預設大小)時的輸出。
使用預設執行緒池大小(4 個執行緒)
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`); });
可以看到總的執行時間有100ms的差異。總執行時間(執行緒池大小 4)為 600 毫秒,總執行時間(執行緒池大小 100)為 700 毫秒。因此,執行緒池大小為 4 花費的時間較少。
為什麼線程數多! =可以同時處理更多任務?
第一個原因是每個執行緒都有自己的堆疊和資源需求。如果增加執行緒數量,最終會導致記憶體或 CPU 資源不足的情況。
第二個原因是作業系統必須調度執行緒。如果線程太多,作業系統將花費大量時間在執行緒之間切換(上下文切換),這會增加開銷並降低效能,而不是提高效能。
現在,我們可以說,這不是增加線程池大小來實現可擴展性和高效能,而是使用正確的架構,例如集群,並了解任務性質(I/O 與CPU 限制) )以及Node. js 的事件驅動模型如何運作。
感謝您的閱讀。
以上是Node.js 內部結構的詳細內容。更多資訊請關注PHP中文網其他相關文章!