首頁 >web前端 >js教程 >Node.js 事件循環內部:深入探究

Node.js 事件循環內部:深入探究

Patricia Arquette
Patricia Arquette原創
2025-01-11 20:29:43962瀏覽

Inside the Node.js Event Loop: A Deep Dive

Node.js 單線程模型探索

Node.js採用事件驅動和非同步I/O的方式,實作了單執行緒、高並發的JavaScript運行環境。既然單執行緒意味著一次只能做一件事,那麼Node.js如何透過一個執行緒實現高並發和非同步I/O呢?本文將圍繞這個問題探討 Node.js 的單線程模型。

高並發策略

一般來說,高並發的解決方案是提供多執行緒模型。伺服器為每個客戶端請求分配一個執行緒並使用同步 I/O。系統透過執行緒切換來彌補同步I/O呼叫的時間成本。例如,Apache就使用這種策略。由於 I/O 操作通常非常耗時,因此這種方法很難獲得高效能。不過,它非常簡單,可以實現複雜的互動邏輯。

事實上,大多數 Web 伺服器端並不執行太多運算。接收到請求後,將請求傳遞給其他服務(例如讀取資料庫),然後等待結果返回,最後將結果傳送給客戶端。因此,Node.js 使用單線程模型來處理這種情況。它不是為每個傳入的請求分配一個線程,而是使用一個主線程來處理所有請求,然後非同步處理 I/O 操作,避免了創建、銷毀線程以及線程之間切換的開銷和複雜性。

事件循環

Node.js 在主執行緒中維護一個事件佇列。當收到請求時,它會作為事件新增到此佇列中,然後繼續接收其他請求。當主執行緒空閒時(沒有請求傳入),它開始循環遍歷事件佇列以檢查是否有事件需要處理。有兩種情況:對於非I/O任務,主執行緒會直接處理,並透過回呼函數傳回上層;對於I/O任務,它會從執行緒池中取出一個執行緒來處理事件,指定一個回呼函數,然後繼續循環佇列中的其他事件。

一旦執行緒中的I/O任務完成,就執行指定的回呼函數,並將完成的事件放在事件佇列的末尾,等待事件循環。當主線程再次循環到這個事件時,直接處理並返回給上層。這個過程稱為Event Loop,其運作原理如下圖所示:

Inside the Node.js Event Loop: A Deep Dive

此圖展示了Node.js的整體運作原理。 Node.js 從左到右、從上到下分為四層:應用層、V8 引擎層、Node API 層、LIBUV 層。

  • 應用層:是JavaScript交互層。常見的範例是 Node.js 模組,例如 http 和 fs。
  • V8引擎層:使用V8引擎解析JavaScript語法,然後與下層API互動。
  • Node API層:為上層模組提供系統調用,通常用C實現,與作業系統互動。
  • LIBUV Layer:是跨平台的底層封裝,實作事件循環、檔案操作等,是Node.js實作非同步的核心。

無論是Linux平台或Windows平台,Node.js內部都使用執行緒池來完成非同步I/O操作,LIBUV統一了不同平台差異的呼叫。所以,Node.js 中的單執行緒僅意味著 JavaScript 在單執行緒中運行,而不是 Node.js 整體是單線程的。

工作原理

Node.js 實作非同步的核心在於事件。也就是說,它將每個任務視為一個事件,然後透過事件循環來模擬非同步效果。為了更具體、更清楚地理解和接受這個事實,我們下面用偽代碼來描述它的運作方式。

1. 定義事件隊列

由於它是一個佇列,所以它是先進先出(FIFO)的資料結構。我們用JS數組來描述,如下:

/**
 * Define the event queue
 * Enqueue: push()
 * Dequeue: shift()
 * Empty queue: length === 0
 */
let globalEventQueue = [];

我們用陣列來模擬佇列結構:陣列的第一個元素是佇列的頭,最後一個元素是佇列的尾端。 push() 在佇列尾部插入一個元素,shift() 從佇列頭部刪除一個元素。這樣就實作了一個簡單的事件隊列。

2.定義請求接收入口

每個請求都會被攔截並進入處理函數,如下圖:

/**
 * Receive user requests
 * Every request will enter this function
 * Pass parameters request and response
 */
function processHttpRequest(request, response) {
    // Define an event object
    let event = createEvent({
        params: request.params, // Pass request parameters
        result: null, // Store request results
        callback: function() {} // Specify a callback function
    });

    // Add the event to the end of the queue
    globalEventQueue.push(event);
}

該函數只是將使用者的請求封裝為一個事件,放入佇列中,然後繼續接收其他請求。

3. 定義事件循環

當主執行緒空閒時,開始循環事件佇列。所以我們要定義一個函數來循環事件佇列:

/**
 * The main body of the event loop, executed by the main thread when appropriate
 * Loop through the event queue
 * Handle non-IO tasks
 * Handle IO tasks
 * Execute callbacks and return to the upper layer
 */
function eventLoop() {
    // If the queue is not empty, continue to loop
    while (this.globalEventQueue.length > 0) {
        // Take an event from the head of the queue
        let event = this.globalEventQueue.shift();

        // If it's a time-consuming task
        if (isIOTask(event)) {
            // Take a thread from the thread pool
            let thread = getThreadFromThreadPool();
            // Hand it over to the thread to handle
            thread.handleIOTask(event);
        } else {
            // After handling non-time-consuming tasks, directly return the result
            let result = handleEvent(event);
            // Finally, return to V8 through the callback function, and then V8 returns to the application
            event.callback.call(null, result);
        }
    }
}

主執行緒持續監聽事件佇列。對於I/O任務,它交給線程池處理,對於非I/O任務,它自己處理並返回。

4. 處理I/O任務

執行緒池收到任務後,直接處理I/O操作,例如讀取資料庫:

/**
 * Define the event queue
 * Enqueue: push()
 * Dequeue: shift()
 * Empty queue: length === 0
 */
let globalEventQueue = [];

當I/O任務完成時,執行回調,將請求結果儲存到事件中,並將事件放回佇列中,等待循環。最後,當前線程被釋放。當主執行緒再次循環到該事件時,直接處理。

總結上面的過程,我們發現Node.js只使用一個主執行緒來接收請求。接收到請求後,不要直接處理,而是放入事件佇列中,然後繼續接收其他請求。當它空閒時,它會透過事件循環處理這些事件,從而達到非同步的效果。當然,對於I/O任務,還是需要依賴系統層面的線程池來處理。

因此,我們可以簡單地理解為 Node.js 本身是一個多執行緒平台,但它在單執行緒中處理 JavaScript 層級的任務。

CPU 密集型任務是個缺點

到目前為止,我們應該對 Node.js 的單線程模型有了一個簡單清晰的認識。它透過事件驅動模型實現高並發和非同步I/O。然而,Node.js 也有不擅長的地方。

如上所述,對於I/O任務,Node.js將其交給執行緒池進行非同步處理,高效且簡單。因此,Node.js 適合處理 I/O 密集型任務。但並非所有任務都是 I/O 密集型的。當遇到CPU密集型任務,也就是只依賴CPU計算的操作,如資料加解密(node.bcrypt.js)、資料壓縮解壓縮(node-tar)時,Node.js會一一處理一。如果前面的任務沒有完成,後面的任務就只能等待。如下圖:

Inside the Node.js Event Loop: A Deep Dive

在事件佇列中,如果前面的CPU運算任務沒有完成,後面的任務就會被阻塞,導致反應緩慢。如果作業系統是單核心的話,可能還可以忍受。但現在大多數伺服器都是多CPU或多核心的,而Node.js只有一個EventLoop,也就是說只佔用一個CPU核。當 Node.js 被 CPU 密集型任務佔用,導致其他任務阻塞時,仍有 CPU 核心閒置,造成資源浪費。

所以,Node.js 不適合 CPU 密集型任務。

應用場景

  • RESTful API:請求和回應只需要少量文本,不需要太多邏輯處理。因此,可以並發處理數萬個連接。
  • 聊天服務:輕量級,流量大,沒有複雜的運算邏輯。

Leapcell:用於 Web 託管、非同步任務和 Redis 的下一代無伺服器平台

Inside the Node.js Event Loop: A Deep Dive

最後介紹一下最適合部署Node.js服務的平台:Leapcell。

1. 多語言支持

  • 使用 JavaScript、Python、Go 或 Rust 進行開發。

2.免費部署無限個項目

  • 只需支付使用費用-無請求,不收費。

3. 無與倫比的成本效益

  • 即用即付,無閒置費用。
  • 範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。

4.簡化的開發者體驗

  • 直覺的使用者介面,輕鬆設定。
  • 完全自動化的 CI/CD 管道和 GitOps 整合。
  • 即時指標和日誌記錄以獲取可行的見解。

5. 輕鬆的可擴充性和高效能

  • 自動擴展,輕鬆處理高並發。
  • 零營運開銷-只需專注於建置。

Inside the Node.js Event Loop: A Deep Dive

在文件中探索更多內容!

Leapcell Twitter:https://x.com/LeapcellHQ

以上是Node.js 事件循環內部:深入探究的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn