簡體   English   中英

Node.js 事件循環

[英]Node.js Event loop

Node.js I/O 事件循環是單線程還是多線程?

如果我有多個 I/O 進程,node 會將它們放在一個外部事件循環中。 它們是按順序處理(最快的先)還是處理事件循環以並發處理它們(......以及在哪些限制中)?

事件循環

Node.js 事件循環在單線程下運行,這意味着您編寫的應用程序代碼在單線程上進行評估。 Nodejs 本身通過 libuv 使用了許多底層線程,但是在編寫 nodejs 代碼時您永遠不必處理這些線程。

每個涉及 I/O 調用的調用都需要您注冊一個回調。 此調用也會立即返回,這允許您在不使用應用程序代碼中的線程的情況下並行執行多個 IO 操作。 一旦 I/O 操作完成,它的回調將被推送到事件循環中。 它將在執行之前在事件循環上推送的所有其他回調后立即執行。

有幾種方法可以對如何將回調添加到事件循環中進行基本操作。 通常你不應該需要這些,但時不時它們會很有用。

永遠不會有兩條真正的並行執行路徑,因此所有操作本質上都是線程安全的。 通常會有幾個異步並發執行路徑由事件循環管理。

閱讀有關事件循環的更多信息

限制

由於事件循環,節點不必為每個傳入的 tcp 連接啟動一個新線程。 這允許節點同時為數十萬個請求提供服務,只要您不計算每個請求的前 1000 個素數。

這也意味着不要進行 CPU 密集型操作很重要,因為這些操作將鎖定事件循環並阻止其他異步執行路徑繼續。 不使用所有 I/O 方法的sync變體也很重要,因為它們也會鎖定事件循環。

如果你想做 CPU 繁重的事情,你應該將它委托給一個不同的進程,它可以更有效地執行 CPU 綁定操作,或者你可以將它編寫為節點本機附加組件

閱讀有關用例的更多信息

控制流

為了管理編寫許多回調,您可能需要使用控制流庫。 我相信這是目前最流行的基於回調的庫:

我使用過回調,它們幾乎讓我發瘋,我使用 Promise 的體驗要好得多,bluebird 是一個非常流行且快速的 Promise 庫:

我發現這是節點社區中一個非常敏感的話題(回調與承諾),因此無論如何,請使用您認為最適合您個人的方式。 一個好的控制流庫還應該為您提供異步堆棧跟蹤,這對於調試非常重要。

當事件循環中的最后一個回調完成它的執行路徑並且不注冊任何其他回調時,Node.js 進程將完成。

這不是一個完整的解釋,我建議您查看以下線程,它是最新的:

我如何開始使用 Node.js

從威廉的回答:

Node.js 事件循環在單個線程下運行。 每個 I/O 調用都需要您注冊一個回調。 每個 I/O 調用也會立即返回,這允許您在不使用線程的情況下並行執行多個 IO 操作。

我想從上面這句話開始解釋,這是我隨處可見的對node js框架的常見誤解之一。

Node.js 不會神奇地只用一個線程處理所有這些異步調用,並且仍然保持該線程不被阻塞。 它在內部使用谷歌的 V8 引擎和一個名為 libuv(用 C++ 編寫)的庫,使其能夠將一些潛在的異步工作委托給其他工作線程(有點像等待從主節點線程委托的任何工作的線程池) )。 然后當這些線程完成它們的執行時,它們會調用它們的回調,這就是事件循環如何知道工作線程的執行已完成這一事實。

nodejs 的主要觀點和優點是您永遠不需要關心那些內部線程,它們將遠離您的代碼!。 通常在多線程環境中發生的所有討厭的同步內容都將被 nodejs 框架抽象出來,您可以在對程序員更友好的環境中愉快地在單線程(主節點線程)上工作(同時受益於多線程的所有性能增強)線程)。

如果有人感興趣,下面是一個很好的帖子: 線程池何時使用?

您必須首先了解 nodeJs 實現才能了解事件循環。

實際上 node js 核心實現使用兩個組件:

  • v8 javascript 運行時引擎

  • libuv 用於處理非 i/o 阻塞操作並為您處理線程和並發操作;

使用 javascript,您實際上可以用一個線程編寫代碼,但這並不意味着您的代碼在一個線程上執行,盡管您可以使用節點 js 中的集群在多個線程上執行

現在當你想執行一些代碼時:

 let fs = require('fs'); fs.stat('path',(err,stat)=>{ //do something with the stat; console.log('second'); }); console.log('first');

  • 這段代碼在高層的執行是這樣的:首先 v8 引擎運行這段代碼,然后如果沒有錯誤,一切都很好,那么它會尋找它,當它到達 fs 時,它會嘗試逐行運行它。 stats 這是一個 node js api,非常類似於 web api,比如 setTimeout 瀏覽器在遇到 fs.stats 時會為我們處理它,它將代碼傳遞給帶有標志的 libuv 組件,並將您的回調傳遞給事件隊列然后你在操作期間執行你的代碼的 libuv,當它完成時只是發送一些信號,然后 v8 執行你的代碼 az 一個你在隊列上設置的回調,但它總是檢查堆棧是否為空然后繼續你的代碼隊列#永遠記住這一點!

好吧,要理解事件中的nodejs I/O事件,你必須正確理解nodejs事件循環。

從名稱事件循環中,我們了解到它是一個循環,循環運行,直到循環中沒有事件或應用程序關閉為止。

事件循環是 nodejs 中最重要的特性之一,它是 nodejs 中異步編程的原因。

當程序啟動時,我們處於事件循環運行的單線程中的節點進程中。 現在我們需要知道的最重要的事情是事件循環是執行回調函數內的所有應用程序代碼的地方。

所以,基本上所有不是頂級代碼的代碼都會在事件循環中運行。 某些部分(主要是繁重的任務)可能會被卸載到線程池( 何時使用線程池? ),事件循環將處理這些繁重的任務並將結果返回給事件循環的事件。

它是 node 架構的核心,nodejs 圍繞回調函數構建。 因此,一旦某些工作在未來某個時間完成,就會觸發回調,因為節點使用事件觸發架構。

當應用程序在節點服務器上收到 HTTP 請求或計時器到期或文件完成讀取所有這些將在完成工作后立即發出事件,然后我們的事件循環將接收這些事件並調用回調與每個事件關聯的函數,通常說事件循環進行編排,這只是意味着它接收事件,調用它們的回調函數,並將更昂貴的任務卸載到線程池。 在此處輸入圖片說明 現在,這一切實際上是如何在幕后運作的? 這些回調以什么順序執行?

好吧,當我們啟動我們的節點應用程序時,事件循環立即開始運行。 一個事件循環有多個階段,每個階段都有一個回調隊列,其中最重要的四個階段是1.過期定時器回調,2.I/O輪詢和回調3.setImmediate回調,4.關閉回調。 Node.js 內部還使用了其他階段。

在此處輸入圖片說明

因此,第一階段處理過期計時器的回調,例如,來自 setTimeout() 函數。 因此,如果有剛剛過期的定時器的回調函數,這些是事件循環要處理的第一個回調函數。

** 最重要的是,如果一個計時器在處理其他階段的過程中稍后到期,那么該計時器的回調只會在事件循環返回到第一個階段時才被調用。 它在所有四個階段都是這樣工作的。**

因此,每個隊列中的回調都會被一個一個處理,直到隊列中沒有回調為止,只有這樣,事件循環才會進入下一階段。 例如,假設有 1000 個 setTimeOut 回調計時器到期並且事件循環處於第一階段,那么所有這 1000 個 setTimeOuts 回調將一個接一個執行,然后將進入下一階段(I/O 池和回調)。

接下來,我們有 I/O 池和 I/O 回調的執行。 這里 I/O 代表輸入/輸出,輪詢基本上意味着尋找准備處理的新 I/O 事件並將主題放入回調隊列。

在 Node 應用程序的上下文中,I/O 主要意味着諸如網絡和文件訪問之類的東西,因此在此階段可能會執行 99% 的通用應用程序代碼。

下一個階段是 setImmediate 回調,SetImmediate 是一種特殊的計時器,如果我們想在 I/O 輪詢和執行階段之后立即處理回調,我們可以使用它。

最后,第四階段是關閉回調,在這個階段,處理所有關閉事件,例如當服務器或 WebSocket 關閉時。

這是事件循環中的四個階段,但是除了這四個回調隊列之外,實際上還有另外兩個隊列, 1. nextTick() 其他 2. 微任務隊列(主要用於已解決的承諾)

在此處輸入圖片說明

如果這兩個隊列之一中有任何回調要處理,它們將在事件循環的當前階段完成后立即執行,而不是等待整個循環/循環完成。

換句話說,在這四個階段的每個階段之后,如果這兩個特殊隊列中有任何回調,它們將立即執行。 現在想象一下,當過期計時器的回調正在運行時,promise 從 API 調用中解析並返回一些數據,在這種情況下,promise 回調將在計時器完成后立即執行。

同樣的邏輯也適用於 nextTick() 隊列。 nextTick() 是一個函數,當我們真的,真的需要在當前事件循環階段之后立即執行某個回調時,我們可以使用它。 它有點類似於 setImmediate,不同的是 setImmediate 只在 I/O 回調階段之后運行。

上述所有事情是否會在事件循環的一個滴答/周期內發生,同時它們的新事件可能在特定階段出現或舊事件可能過期,事件循環將用另一個新周期處理這些事件。

所以現在是時候決定循環是否應該繼續到下一個滴答聲或者程序是否應該退出。 Node 只是檢查是否有任何計時器或 I/O 任務仍在后台運行,如果沒有,它將退出應用程序。 但是如果有任何掛起的計時器或 I/O 任務,那么節點將繼續運行事件循環並開始下一個循環。

在此處輸入圖片說明

例如,在 node 應用程序中,當我們偵聽傳入的 HTTP 請求時,我們基本上運行一個無限 I/O 任務,並且在事件循環中運行,因為 Node.js 繼續運行並繼續偵聽傳入的新 HTTP 請求而不是僅僅退出應用程序。

此外,當我們在后台寫入或讀取一個文件時,該文件也是一個 I/O 任務,當它使用該文件時,應用程序不存在是有道理的,對吧?

現在實踐中的事件循環:

const fs = require('fs');
setTimeout(()=>console.log('Timer 1 finished'), 0);
fs.readFile('test-file.txt', ()=>{
    console.log('I/O finished');
});
setImmediate(()=>console.log('Immediate 1 finished'))
console.log('Hello from the top level code');

輸出: 在此處輸入圖片說明 好吧,第一個 lin 是來自頂級代碼的 Hello ,是的,這是預期的,因為這是一個立即執行的代碼。 然后在我們有三個輸出之后, Timer 1 完成這一行是因為我們之前討論的第一階段,但是在I/O 完成之后應該打印,因為我們討論的是 setImmediate 在 I/O 回調階段之后運行,但是這個代碼實際上不在 I/O 循環中,因此它不在事件循環內運行,因為它不在任何回調函數內運行。

現在讓我們做另一個測試:

const fs = require('fs');
setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));

fs.readFile('test-file.txt', ()=>{
    console.log('I/O finished');
    setTimeout(()=>console.log('Timer 2 finished'), 0);
    setImmediate(()=>console.log('Immediate 2 finished'));
    setTimeout(()=>console.log('Timer 3 finished'), 0);
    setImmediate(()=>console.log('Immediate 3 finished'));
});
console.log('Hello from the top level code')

輸出: 在此處輸入圖片說明

輸出是否符合預期? 現在讓我們添加一些延遲:

setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));

fs.readFile('test-file.txt', ()=>{
    console.log('I/O finished');
    setTimeout(()=>console.log('Timer 2 finished'), 3000);
    setImmediate(()=>console.log('Immediate 2 finished'));
    setTimeout(()=>console.log('Timer 3 finished'), 0);
    setImmediate(()=>console.log('Immediate 3 finished'));
});
console.log('Hello from the top level code')

輸出: 在此處輸入圖片說明

在 I/O 內部的第一個循環中,所有內容都執行了,但由於在第二個循環中在其代碼中執行了交易的 Timer-2。

現在讓我們添加 nextTick(),看看 nodejs 的行為:

setTimeout(()=>console.log('Timer 1 finished'), 0);
setImmediate(()=>console.log('Immediate 1 finished'));

fs.readFile('test-file.txt', ()=>{
    console.log('I/O finished');
    setTimeout(()=>console.log('Timer 2 finished'), 3000);
    setImmediate(()=>console.log('Immediate 2 finished'));
    setTimeout(()=>console.log('Timer 3 finished'), 0);
    setImmediate(()=>console.log('Immediate 3 finished'));
    process.nextTick(()=>console.log('Process Next Tick'));
});
console.log('Hello from the top level code')

輸出:

在此處輸入圖片說明

那么,第一個回調是在 process.NextTick() 內部執行的,正如預期的那樣嗎? 因為 nextTicks 回調停留在微任務隊列中,它們在每個階段之后執行。

如果你運行這個簡單的節點代碼

console.log('starting')
setTimeout(()=>{
    console.log('0sec')
}, 0)
setTimeout(()=>{
    console.log('2sec')
}, 2000)
console.log('end')

你期望輸出是什么? 如果它是,

starting 
0sec
end 
2sec 

猜錯了,我們會得到

starting 
end 
0sec 
2sec 

因為節點在退出 main() 之前永遠不會在事件循環中打印代碼

所以基本上,首先main()將進入堆棧,然后console.log('starting ')所以你會看到它首先打印出來,然后是setTimeout(()=>{console.log('0sec')}, 0)將進入堆棧,然后進入 nodeAPI(節點使用多線程(用 C++ 編寫的 lib)執行 setTimeout 以完成,即使上面的代碼是單線程代碼)時間到了它移動到事件循環,現在節點除非堆棧不為空,否則無法打印它。 因此,下一行即 2 秒的 setTimeout 將首先被推送到堆棧,然后 nodeAPI 將等待 2 秒完成,然后甚至循環,這意味着將執行下一行代碼,即 console.log('end' ),所以我們在 0 秒之前看到結束 msg,因為如果節點非阻塞性質。 在結束代碼結束后,main 被彈出,它的事件循環代碼將被執行,它是第一個 0 秒,之后 2 秒 msg 將被打印。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM