Node.js 運(yùn)行機(jī)制:Event Loop

并發(fā)模型

常見的并發(fā)模型是并行工作者模型,任務(wù)分配給多個(gè)工作者,每個(gè)工作者完成整個(gè)任務(wù),常說的 C 語言的多線程就是這種模型,它的工作模式如下圖。

多線程并行工作模式圖

而 Node.js 用的并發(fā)模型是事件驅(qū)動(dòng)模型,工作者對出現(xiàn)的事件做出反應(yīng),自身也能產(chǎn)生事件,它的工作模式如下圖。

事件驅(qū)動(dòng)工作模式圖

單線程、同異步

常說的 JavaScript 的單線程指的是用戶代碼執(zhí)行上的單線程,即同一時(shí)間只能執(zhí)行一段代碼,這與 ?C 語言同一時(shí)間可以并行執(zhí)行多段代碼形成鮮明的對比。
所以 Node.js 的執(zhí)行可以簡單地分成兩個(gè)階段:

  • 初始化代碼執(zhí)行
  • 事件循環(huán)

初始化代碼執(zhí)行里,執(zhí)行所有的同步操作代碼。所謂同步操作,就是永遠(yuǎn)一步步執(zhí)行、沒有結(jié)果不繼續(xù)執(zhí)行后面代碼的操作。對應(yīng)的異步操作是不等待結(jié)果就繼續(xù)執(zhí)行后面代碼的操作。一般異步操作都帶有一個(gè)回調(diào)函數(shù),而回調(diào)函數(shù)里的操作不包括在上面說的「后面代碼」里,而是異步操作完成以后希望要執(zhí)行的操作,它們需要排隊(duì)等待被執(zhí)行。

異步操作的回調(diào)函數(shù)排隊(duì)等待被執(zhí)行就算在事件循環(huán)這一階段。在執(zhí)行完所有同步代碼以后,Node.js 查看回調(diào)隊(duì)列里有沒有任務(wù),有的話就執(zhí)行,沒有的話就等待異步操作完成,因?yàn)閹в谢卣{(diào)任務(wù)的異步操作完成時(shí)會(huì)將回調(diào)任務(wù)入隊(duì)到回調(diào)隊(duì)列,這樣就有任務(wù)可以執(zhí)行了。所以可以很自然地推理出,如果回調(diào)隊(duì)列為空且沒有需要等待完成的異步操作,這個(gè) Node.js 進(jìn)程就結(jié)束了。事實(shí)也是如此。

由上也可以知道,所有的用戶代碼最終都是在同一線程也就是主線程上面順序執(zhí)行的。而回調(diào)函數(shù)就是執(zhí)行順序不是按聲明順序來執(zhí)行而是要經(jīng)過 Node.js 的事件循環(huán)來安排執(zhí)行的用戶代碼。

Node.js 異步操作的執(zhí)行

我們知道 Node.js 的所有異步操作都是由 Libuv 來負(fù)責(zé)的。Libuv 將可以給系統(tǒng)內(nèi)核來執(zhí)行的異步操作都交給了系統(tǒng)內(nèi)核來執(zhí)行,只有當(dāng)系統(tǒng)不能執(zhí)行這個(gè)操作的時(shí)候才會(huì)用自己的線程池來執(zhí)行這個(gè)異步操作。下圖列出了一些異步操作一般由誰來執(zhí)行:(圖來自:Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM

異步操作執(zhí)行分類圖

事件循環(huán)順序

事件循環(huán)圖

如上圖,每一個(gè)方框代表一個(gè)事件循環(huán)階段,每一階段都有自己的先進(jìn)先出的任務(wù)隊(duì)列。從用戶代碼入口開始,執(zhí)行完所有同步代碼后進(jìn)入事件循環(huán),在事件循環(huán)里的每一個(gè)階段都查看該階段的任務(wù)隊(duì)列是否為空,如果不為空則嘗試同步執(zhí)行(以先進(jìn)先出順序一個(gè)一個(gè)執(zhí)行)所有隊(duì)列里的任務(wù)直到隊(duì)列為空。這里輪詢事件階段的任務(wù)執(zhí)行有最大次數(shù)限制。之后會(huì)細(xì)講。

實(shí)際上事件循環(huán)里包含的階段比圖上列出的多,但是我們應(yīng)該關(guān)心的都在圖上列出來了。

setTimeout、setInterval

setTimeoutsetInterval 調(diào)度的回調(diào)任務(wù)在這里排隊(duì)執(zhí)行。

I/O

像是由網(wǎng)絡(luò)、磁盤數(shù)據(jù)、子進(jìn)程等 I/O 類調(diào)度的回調(diào)任務(wù)在這里排隊(duì)執(zhí)行。

輪詢事件

查看是否有新的 I/O 事件,為下個(gè)輪詢的 I/O 階段提供任務(wù)。
如果所有隊(duì)列為空,這里阻塞主線程進(jìn)入沉睡,直到發(fā)生以下事件之一:

  • 有新的 I/O 事件發(fā)生
  • 有子線程完成任務(wù)
  • 有定時(shí)器達(dá)到閾值

也就是說,上面的事件的發(fā)生都會(huì)進(jìn)入這階段的事件任務(wù)隊(duì)列,當(dāng)事件隊(duì)列不為空時(shí)就執(zhí)行到空或達(dá)到最大次數(shù)限制(因?yàn)檫@階段在處理事件的時(shí)候可以產(chǎn)生新事件入隊(duì)而導(dǎo)致隊(duì)列一直不為空從而阻塞事件循環(huán),所以有最大次數(shù)限制)。

setImmediate

通過 setImmediate 設(shè)置的回調(diào)在這里排隊(duì)執(zhí)行。

'close' 事件

on('close') 事件調(diào)用的回調(diào)在這里排隊(duì)執(zhí)行。

setTimeout/setImmediate

對于在非 I/O 回調(diào)里的 setTimeoutsetImmediate 來說,執(zhí)行的先后順序無法確定,而在 I/O 回調(diào)里 setImmediate 總是比 setTimeout 先執(zhí)行。
如在主模塊里的這段代碼:

setTimeout(() => {
  console.log('in setTimeout')
}, 0)

setImmediate(() => {
  console.log('in setImmediate')
})

運(yùn)行結(jié)果可能是:

in setTimeout
in setImmediate

也可能是:

in setImmediate
in setTimeout

而下面這段代碼:

const fs = require('fs');

fs.readFile(filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

setImmediatesetTimeout 之前。

特殊的 process.nextTick() 和 Promise.resolve()

process.nextTick()Promise.resolve() 不在上面的循環(huán)圖里的階段里面,它們也有一個(gè)自己的任務(wù)隊(duì)列,在每個(gè)階段結(jié)束的時(shí)候都會(huì)查看這個(gè)隊(duì)列是否為空,如果不為空就一個(gè)個(gè)執(zhí)行里面所有的任務(wù)直到隊(duì)列為空。
執(zhí)行邏輯大概如下圖:

事件循環(huán)階段間隙圖

顯然在遞歸調(diào)用 process.nextTick()Promise.resolve() 的時(shí)候任務(wù)隊(duì)列一直不為空則會(huì)引起阻塞,但是它們的存在又確實(shí)是必要的:

  • 用戶要在事件循環(huán)繼續(xù)之前處理錯(cuò)誤、清理資源
  • 在當(dāng)前執(zhí)行棧之后且在事件循環(huán)之前需要執(zhí)行一個(gè)回調(diào)

官方文檔舉了這樣一個(gè)例子:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event'); // 這里對 `event` 事件的監(jiān)聽還沒運(yùn)行到,則這個(gè) emit 不能觸發(fā)對應(yīng)的回調(diào)
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

用了 process.nextTick() 后:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // 先執(zhí)行了所有同步代碼然后才執(zhí)行 process.nextTick 的回調(diào)即 emit 一個(gè) event 事件
  process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

參考:
并發(fā)模型
The Node.js Event Loop, Timers, and process.nextTick()
What you should know to really understand the Node.js Event Loop
Morning Keynote- Everything You Need to Know About Node.js Event Loop - Bert Belder, IBM
Understanding the Node.js Event Loop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容