Node Event Loop

NodeJS 于其它任何平臺(tái)的區(qū)別在于它如何處理 I/O。當(dāng)我們聽到 NodeJS 被一些人介紹的時(shí)候總是說:非阻塞,基于 google v8 js 引擎的事件驅(qū)動(dòng)平臺(tái)。這些意味著什么?”非阻塞“和”事件驅(qū)動(dòng)“是什么意思?

所有的這些都在于 NodeJS 的核心,即 Event Loop.在接下來的一些列內(nèi)容中,我將描述event loop 是什么,它如何工作,它如何作用于我們的應(yīng)用,如何更好去應(yīng)用它以及更多。為什么是一系列內(nèi)容而不是一個(gè)?因?yàn)檫@個(gè)真的說來話長(zhǎng)我肯定會(huì)有一些遺漏,所以我將寫一個(gè)系列。在這第一篇文章中,我將描述 NodeJS 如何工作,它如何獲取 I/O 以及如何運(yùn)行在不同的平臺(tái)上等等。

Reactor 模式:

NodoJS 在事件驅(qū)動(dòng)模型工作時(shí)需要 Event Demultiplexer 和事件隊(duì)列。所有的 I/O 請(qǐng)求將會(huì)最終生成一個(gè)完成或者失敗的事件或者其它的觸發(fā),這些都叫做 Event(事件)。這些事件的進(jìn)行是遵循一定的算法規(guī)則的。

  1. Event demultiplexer 接收 I/O 請(qǐng)求然后將他們分發(fā)到合適的硬件上。
  2. 一旦處理了 I/O 請(qǐng)求(比如 可以讀取來自文件的數(shù)據(jù), 可以讀取來自套接字的數(shù)據(jù)等等),event demultiplexer 將為隊(duì)列中的特定動(dòng)作添加已注冊(cè)的回調(diào)處理程序。這些回調(diào)事件被稱為 events 以及這個(gè)被添加事件的隊(duì)列被稱作事件隊(duì)列。
  3. 當(dāng)事件在隊(duì)列中準(zhǔn)備好被執(zhí)行的時(shí)候,它們會(huì)按照接收順序依次執(zhí)行,直到隊(duì)列為空。
  4. 如果事件隊(duì)列中沒有事件了,或者 Event Demultiplexer 沒有任何待處理的請(qǐng)求了,程序?qū)?huì)完成,否則,進(jìn)程會(huì)從第一步繼續(xù)執(zhí)行。

編排整個(gè)機(jī)制的程序被稱作事件循環(huán)(Event Loop)

事件循環(huán)是單線程(single threaded)并且半無限的循環(huán)。它被稱為半無限循環(huán)的原因是因?yàn)楫?dāng)沒有更多的工作要做時(shí),實(shí)際上會(huì)在某個(gè)時(shí)刻退出。 從開發(fā)人員的角度來看,這是程序退出的地方。

note: 不要混淆了事件循環(huán)和事件派發(fā)(Event Emitter)。事件派發(fā)完全和這個(gè)循環(huán)機(jī)制是不一樣的概念。接下去的一篇文章中,我將解釋事件派發(fā)如何通過事件循環(huán)去影響事件處理過程。

event loop.jpeg

上面的示意圖是一個(gè)描述 NodeJS 如何工作的高度概覽,以及展示了這個(gè)設(shè)計(jì)模式中的主要組成。該設(shè)計(jì)模式稱為 Reactor Pattern ,實(shí)際上會(huì)更復(fù)雜。那么到底有多復(fù)雜呢?

tips:

  • Event demultiplexer 并不是一個(gè)單一的組件去做所有操作系統(tǒng)上的所有類型的 I/O 操作。
  • 事件隊(duì)列也不是一件單一的像這邊展示的隊(duì)列一樣。其中所有類型的事件都會(huì)被放入以及出隊(duì)列,I/O 不是唯一的會(huì)被放進(jìn)隊(duì)列的事件類型。

所以讓我們?cè)偕钊肓私庖幌隆?/p>

Event Demultiplexer

Event Demultiplexer 不是一個(gè)現(xiàn)實(shí)世界中真是存在的組件實(shí)物,而是一個(gè)在 reactor 模式中的抽象概念。在現(xiàn)實(shí)世界中,event dumultiplexer 在不同的系統(tǒng)中的實(shí)現(xiàn)被叫做不同的名字,比如 Linux 中被叫做 epoll,BSD 系統(tǒng)(MacOS)中的 kqueue,Solaris 的 event ports,Windows 中的 IOCP 等等。NodeJS 使用這些實(shí)現(xiàn)的低級(jí)非阻塞,異步硬件 I/O 功能。

復(fù)雜的文件 I/O(Complexities in File I/O)

令人困惑的事實(shí)是,不是所有的類型的 I/O 都可以使用這些實(shí)現(xiàn)來執(zhí)行。即使是在同樣的操作系統(tǒng)平臺(tái)上,也存在很多復(fù)雜的對(duì)不同 I/O 類型的支持。通常,網(wǎng)絡(luò) I/O 可以使用這些 epoll、kqueue、事件端口和 IOCP以非阻塞方式執(zhí)行,但文件 I/O 卻要復(fù)雜得多。某些系統(tǒng)(Certain systems)如 Linux 不支持文件系統(tǒng)訪問的完全異步。在 MacOS 系統(tǒng)中,使用 kqueue 處理文件系統(tǒng)事件通知/信令會(huì)存在一些存在限制(您可以在此處閱讀有關(guān)這些復(fù)雜情況的更多信息here)解決所有這些文件系統(tǒng)復(fù)雜性以提供完全異步是非常復(fù)雜且?guī)缀醪豢赡艿摹?/p>

復(fù)雜的 DNS

與文件 I/O 類似,Node API 提供的特定 DNS 功能也有特定的復(fù)雜性,由于 NodeJS DNS 函數(shù)比如 dns.lookup 獲取系統(tǒng)配置文件比如 nsswitch.conf, resolv.conf 以及 /etc/hosts,上述描述的文件系統(tǒng)復(fù)雜性一樣也適用于 dxn.resolve 功能。

解決方式?

因此,引入了一個(gè)線程池被用于支持 I/O 函數(shù),但是這些函數(shù)不能由硬件異步 I/O 工具比如 epoll/kqueue/event ports 或者 IOCP 直接尋址?,F(xiàn)在我們知道不是所有的 I/O 函數(shù)會(huì)在線程池中執(zhí)行。NodeJS 已盡最大努力使用非阻塞和異步硬件 I/O 來完成大部分 I/O,但是對(duì)于阻塞或者更復(fù)雜的 I/O 類型【注:此處有一個(gè) complex to address,更復(fù)雜的尋址?】,需要用到線程池。

總結(jié)

正如我們所見,在現(xiàn)實(shí)世界中,在所有的不同類型的操作系統(tǒng)平臺(tái)上支持所有類型的 I/O(文件 I/O,網(wǎng)絡(luò) I/O,DNS 等等)是非常難的,一些 I/O 可以直接在原生的硬件設(shè)施上被支持,并且保留完全的異步,但是也有一些 I/O 類型需要在線程池中被執(zhí)行來達(dá)到異步。

tips: 關(guān)于 Node,很多開發(fā)者都有一個(gè)常見的誤解是 Node 執(zhí)行線程池中的所有 I/O.

為了在支持跨平臺(tái) I/O 的同時(shí)管理整個(gè)過程,應(yīng)該有一個(gè)抽象層,它封裝了這些平臺(tái)間和平臺(tái)內(nèi)的復(fù)雜性,并為Node 的上層暴露出一個(gè)通用的 API。

所以誰做了這個(gè),女士們先生們,有請(qǐng):libuv

從 libuv 的官方文檔中,提到:

  • libuv 是最初為 NodeJS 編寫的跨平臺(tái)支持庫(kù),它是圍繞事件驅(qū)動(dòng)異步 I/O 模型來設(shè)計(jì)的。
  • 該庫(kù)提供的不僅僅是對(duì)不同 I/O 輪詢機(jī)制(polling mechanisms)的簡(jiǎn)單抽象:'handle' 和 'streams' 為套接字和其他實(shí)體(entities)提供高級(jí)抽象; 除此之外,還提供跨平臺(tái)文件 I/O 和線程功能。

現(xiàn)在讓我看看 libuv 的組成。下面的示意圖來自于 libuv 的官網(wǎng)文檔,描述了不同類型的 I/O 是如何通過被暴露的通用 API 處理的。

![libuv 2.png](https://upload-images.jianshu.io/upload_images/4951281-4a563f8ae1b61069.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

現(xiàn)在我們知道 Event Demultiplexer,不是一個(gè)原子實(shí)體【注:其實(shí)這里就是指不是最小的粒度了】,而是一個(gè)由 Libuv 抽象并暴露給 NodeJS 上層的 I/O 處理 API 的集合。它不僅是 libuv 提供給 Node 的 event demultiplexer。Libuv 為 NodeJS 提供了完整的事件循環(huán)功能,包括事件隊(duì)列機(jī)制。

現(xiàn)在讓我們看一下事件隊(duì)列。

事件隊(duì)列(Event Queue)

事件隊(duì)列被認(rèn)為是一個(gè)所有事件被排列以及通過事件循環(huán)機(jī)制執(zhí)行直到隊(duì)列清空的數(shù)據(jù)結(jié)構(gòu),但是在 Node 中是如何發(fā)生的和抽象 reactor 模式的描述完全不一致的。所以哪里不一樣呢?

tips:

  • NodeJS 中有多個(gè)隊(duì)列,不同類型的事件在不同的隊(duì)列中排隊(duì)。
  • 在處理一個(gè)階段后并且在移動(dòng)到下一個(gè)階段之前,事件循環(huán)將會(huì)處理兩個(gè)中間隊(duì)列,直到?jīng)]有任何一個(gè)項(xiàng)目被遺留在中間隊(duì)列中。

所以到底有多少個(gè)隊(duì)列呢?這些中間隊(duì)列是什么呢?

原生的 Libuv 事件循環(huán)中主要處理四個(gè)隊(duì)列類型

  • 過期的計(jì)時(shí)器及 intervals 隊(duì)列(Expired timers and intervals queue)-- 通過使用 setTimeout 添加的 expired timers 或者由 interval 函數(shù)添加 setInterval 組成的 interval 函數(shù)。
  • IO 事件隊(duì)列(IO Event Queue) — 完整的 IO 事件
  • Immediates 隊(duì)列(Immediates Queue) — 通過 setImmediate 函數(shù)添加的回調(diào)函數(shù)
  • Close Handlers 隊(duì)列(Close Handlers Queue) -- 任何 Close Handlers

tips: 請(qǐng)注意,盡管我為了簡(jiǎn)單起見,將所有的這些都稱為“隊(duì)列”,但是實(shí)際上他們都會(huì)有不同的數(shù)據(jù)結(jié)構(gòu)(比如計(jì)時(shí)器是存儲(chǔ)在最小堆中(min-heap))

除了這四種隊(duì)列,還有2個(gè)我剛才提到的有趣的中間隊(duì)列被 Node 執(zhí)行。雖然這些隊(duì)列不是 libuv 自己的一部分,但是是 NodeJS 的一部分,它們是:

  • Next Ticks Queue -- 通過 process.nextTick 函數(shù)添加的回調(diào)函數(shù)
  • 其它微服務(wù)隊(duì)列(Other Microtasks Queue) -- 包括其它微服務(wù)比如 peomise 里的 resolved 回調(diào)函數(shù)

它是如何工作的呢?

正如你所見下面的示例圖,Node 通過在計(jì)時(shí)器中檢查任何過期計(jì)時(shí)器開始事件循環(huán),并在每個(gè)步驟中瀏覽每個(gè)隊(duì)列,同時(shí)保持要處理的總項(xiàng)目的參考計(jì)數(shù)器(and go through each queue in each step while maintaining a reference counter of total items to be processed)。在處理完關(guān)閉處理隊(duì)列(close handlers queue)的進(jìn)程后,如果隊(duì)列中沒有任何項(xiàng)目要執(zhí)行,這個(gè)循環(huán)將會(huì)退出。每個(gè)在事件循環(huán)中執(zhí)行的隊(duì)列可以被看做是事件循環(huán)中的一個(gè)階段。

event loop2.png

紅色描述的中間隊(duì)列比較有趣的一點(diǎn)是,一旦一個(gè)階段完成了,事件循環(huán)將會(huì)檢查兩個(gè)中間隊(duì)列是否有任何可用的項(xiàng)目。如何這里有任何可用項(xiàng)目在中間隊(duì)列,事件循環(huán)將會(huì)馬上開始執(zhí)行它們直到兩個(gè)中間隊(duì)列被清空。一旦它們被清空,事件循環(huán)將會(huì)繼續(xù)執(zhí)行執(zhí)行下一個(gè)階段。

栗子:事件循環(huán)當(dāng)前執(zhí)行了有5個(gè)事件的 immediates 隊(duì)列(immediates queue)。同時(shí),兩個(gè)事件被添加到 next tick 隊(duì)列中,一旦事件循環(huán)完成了在 immediates 隊(duì)列中的五個(gè)事件,事件循環(huán)將會(huì)在移動(dòng)到下一個(gè) close handlers 隊(duì)列之前發(fā)現(xiàn)這里有兩個(gè)項(xiàng)目在 next tick 隊(duì)列中需要被執(zhí)行。然后它將會(huì)執(zhí)行所有 next tick 隊(duì)列中的事件,再移動(dòng)到 close handlers 隊(duì)列中。

Next tick 隊(duì)列和其他 Microtasks

Next tick 隊(duì)列比其它微任務(wù)隊(duì)列擁有更高的優(yōu)先級(jí)。雖然它們都是事件循環(huán)中的一環(huán)。當(dāng) libuv 在階段結(jié)束時(shí)回傳給更高層的 Node。你注意到我用暗紅色表示 next tick 隊(duì)列,表示在開始 microtasks 隊(duì)列中的 promise sesolved 進(jìn)程結(jié)束前, next tick 隊(duì)列將會(huì)清空。

tips:

  • next tick 隊(duì)列的優(yōu)先級(jí)高于 promises resolved 僅僅是在 v8 引擎提供的原生 JS Promises 適用。如果你使用了如 q 或者 bluebird 之類的庫(kù),你會(huì)發(fā)現(xiàn)會(huì)出現(xiàn)完全不一樣的結(jié)果,因?yàn)樗麄儍?yōu)先于原生的 promise 并且有不同的語義。
  • q 和 bluebird 同樣有不同的處理 prosemise resolved 的處理方式,我將會(huì)在之后的文章中解釋。

這些被稱作"中間"隊(duì)列的通常也會(huì)引入一個(gè)新的問題,IO 饑餓(IO starvation)。到處通過 process.nextTick 函數(shù)填充 next tick 隊(duì)列將會(huì)強(qiáng)制事件循環(huán)進(jìn)程一直處理 next tick 隊(duì)列而不是向前移動(dòng)。這個(gè)將會(huì)導(dǎo)致 IO starvation ,因?yàn)槭录h(huán)不能執(zhí)行下一步除非 next tick 隊(duì)列被清空。

tips: 為了避免這個(gè),他們通過用 process.maxTickDepth 參數(shù)給 next tick 隊(duì)列設(shè)置最大限制,但是它由于某些原因在 NodeJS v0.12 被移除了。

我將在接下去的幾篇文章用一些例子深入講解這些隊(duì)列。

最后,現(xiàn)在你知道事件循環(huán)是什么了,以及它如何實(shí)現(xiàn)以及 Node 如何處理異步I/O?,F(xiàn)在讓我們來看一下在 NodeJS 架構(gòu)中 Libuv 處于哪個(gè)位置。

原文鏈接:https://jsblog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810

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

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

  • 20180416分享: 坊間有一句話:江山易改,本性難移。可見要改變一個(gè)人的習(xí)性有多不容易。 學(xué)過大腦科學(xué)...
    讓愛在每一天閱讀 269評(píng)論 0 0
  • 突然想找你聊天 打開窗口 突然發(fā)現(xiàn)上次的結(jié)尾是我 你沒有回 欲言又止了 晚安 新年快樂
    池魚Y閱讀 216評(píng)論 0 0
  • 【一頁一世界.修學(xué)報(bào)告】 報(bào)告人:李筱鳳 報(bào)告時(shí)間:2018年1月15日 今日閱讀《孝經(jīng)》第9頁,心得體會(huì)報(bào)告如下...
    李少鳳閱讀 172評(píng)論 0 0
  • 這一周時(shí)間填充的滿滿的,一直在讀書、讀得到專欄還有子而坐之間忙碌著,是成長(zhǎng)最充實(shí)的一周。這周五才開始看船長(zhǎng)本周的書...
    南卡多杰閱讀 267評(píng)論 0 1

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