新聞中心
這次我們來聊聊 Node.js 里面涉及到的一個(gè)核心概念:event-loop 。只有理解了它,才能明白 node 的進(jìn)程模型,也才能明白異步調(diào)用在實(shí)現(xiàn)層面是什么樣子的,更能明白當(dāng)同步代碼和異步代碼混雜在一起的時(shí)候,CPU 到底跑到我們代碼的哪一行了。文章分為兩篇:event-loop 篇和 Promise/Generator/async 篇。今天我們關(guān)注 event-loop 部分。

在海城等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站設(shè)計(jì)、網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需定制開發(fā),公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),成都品牌網(wǎng)站建設(shè),網(wǎng)絡(luò)營銷推廣,外貿(mào)網(wǎng)站制作,海城網(wǎng)站建設(shè)費(fèi)用合理。
1、代碼思考
我寫了兩個(gè)函數(shù),函數(shù)內(nèi)部直接用 while(true){} 寫了一段死循環(huán)代碼。我們先來思考下面這段 Node.js code 執(zhí)行結(jié)果是什么?很多人說 Node.js 是單線程的。如果是這樣,那 CPU 會(huì)不會(huì)陷入到 whileLoop_1() 的 while 循環(huán)里面出不來?
'use strict';
async function sleep(intervalInMS)
{
return new Promise((resolve,reject)=>{
setTimeout(resolve,intervalInMS);
});
}
async function whileLoop_1(){
while(true){
try {
console.log('new round of whileLoop_1');
await sleep(1000); // LINE-A
} catch (error) {
// ...
}
}
}
async function whileLoop_2(){
while(true){
try {
console.log('new round of whileLoop_2');
await sleep(1000); // LINE-B
} catch (error) {
// ...
}
}
}
whileLoop_1(); // LINE-C
whileLoop_2(); // LINE-D
不賣關(guān)子了,我先把執(zhí)行結(jié)果發(fā)出來。
new round of whileLoop_1
new round of whileLoop_2
new round of whileLoop_1
new round of whileLoop_2
new round of whileLoop_1
new round of whileLoop_2
new round of whileLoop_1
new round of whileLoop_2
new round of whileLoop_1
new round of whileLoop_2
...
是的,正如你所見。這兩個(gè) while 循環(huán)分別在交替執(zhí)行, CPU 也沒有陷入到死循環(huán)里面出不來。那么問題來了:
- CPU 執(zhí)行到 LINE-A 的時(shí)候發(fā)生了什么使得它能成功脫身并有機(jī)會(huì)執(zhí)行 whileLoop_2 ?
- CPU 執(zhí)行到 LINE-B 后,為什么又能回到 whileLoop_1 中繼續(xù)執(zhí)行呢?
2、event-loop
在回答上面的問題前,我們需要先來看一個(gè)至關(guān)重要的概念:event-loop 。其實(shí)我們平時(shí)說 Node.js 是單線程僅僅是指 node 執(zhí)行我們的 JS 代碼,更準(zhǔn)確地說是 V8 執(zhí)行 JS code 是發(fā)生在單線程里面的。實(shí)際上如果你打開 node 進(jìn)程,會(huì)發(fā)現(xiàn)它有不少 worker thread。這是一個(gè)典型的單進(jìn)程多線程模型。這些 worker thread 被放置于線程池里面,而 V8 執(zhí)行 JS code 的線程被稱為主線程。主線程和線程池的配合關(guān)系如下圖所示。主線程負(fù)責(zé)執(zhí)行 JS code ,線程池里面的 worker thread 負(fù)責(zé)執(zhí)行類似訪問 DB、訪問文件這樣的耗時(shí)費(fèi)力的工作,它倆通過消息隊(duì)列協(xié)調(diào)工作。這和餐館工作流程類似。餐館由一個(gè)長得漂亮的小姐姐招呼客人落座并負(fù)責(zé)收集來自各個(gè)餐桌的點(diǎn)單。每當(dāng)收到一個(gè)點(diǎn)好的菜單時(shí),小姐姐會(huì)迅速地把它通過一個(gè)小窗口遞交給后廚。后廚那里有一個(gè)小看板,所有的點(diǎn)單都被陳列在看板上。廚師長根據(jù)訂單的時(shí)間和菜品安排不同的廚師燒菜。菜燒好后,再由小姐姐負(fù)責(zé)上菜。
圖 1:Node.js 單進(jìn)程多線程模型
嗯上面這張圖還是太簡(jiǎn)單了,用來騙新手可以,我知道滿足不了你們。我們把它放大一些。下圖中左邊是主線程,右邊是線程池和一個(gè) worker thread,中間是消息隊(duì)列。
圖 2:Node.js 主線程和工作線程關(guān)系圖
(1)主線程
主線程只干一件事:拼命地執(zhí)行 JS code,做 non-blocking I/O 操作。這些 JS code 既包含我們自己寫的,也包含我們所依賴的 npm package 。這里提到的 non-blocking I/O 操作意味著主線程干的事情基本上都是非阻塞型的工作,例如對(duì) 2+3 求和,迭代數(shù)組等。主線程以 tick 為粒度工作。是的,你一定聽說過 process.nextTick() ,所謂 next tick 就是下一次執(zhí)行 tick 的時(shí)機(jī)。每個(gè) tick 又包含若干個(gè) phase ,按照 Node.js 官網(wǎng)介紹,目前為止一共有 6 個(gè) Phase。
- timers: 這個(gè) phase 執(zhí)行通過 setTimeout()? 和 setInterval() 所設(shè)置的 callback 函數(shù)。
- pending callbacks: 這個(gè) phase 執(zhí)行一些與系統(tǒng)操作相關(guān)的 callback,比如建立 TCP 連接時(shí)收到的 ECONNREFUSED 相關(guān)的 callback 。
- idle, prepare: 僅供Node.js內(nèi)部使用。
- poll: 從消息隊(duì)列里面獲取新的 I/O event,執(zhí)行相應(yīng)的 callback (不包括 setImmediate / close callback / 以及 timer 所設(shè)置的 callback)。
- check: 執(zhí)行通過 setImmediate()? 所設(shè)置的callback.
- close callbacks: 執(zhí)行一些 close callback ,比如通過這樣的代碼 socket.on('close', ...)? 所設(shè)置的 callback。
絕大部分情況下,這些 callback 是用 JS 寫的,Node 通過 Google V8 engine ,在主線程里面來執(zhí)行這些 callback 。我們把上面的 6個(gè) phase 和 tick 的關(guān)系放置到時(shí)間軸上,或許能更形象地說明主線程所做的工作。
圖 3:Node.js 主線程時(shí)序圖
(2)消息隊(duì)列
主線程不單單是在執(zhí)行 JS code,也不僅僅只是在做 non-blocking I/O 操作。它在執(zhí)行代碼的過程中,還會(huì)產(chǎn)生各種各樣的異步請(qǐng)求。直觀一點(diǎn)的如通過 setImmediate(callback[, ...args]) / fs.readFile(path[, options], callback) 產(chǎn)生,晦澀一點(diǎn)的如通過 Promise / async 產(chǎn)生。這些異步請(qǐng)求大部分情況下有一些共性:需要耗費(fèi)一定的時(shí)間去處理。讓主線程放著其它事情不管,傻傻地干等這次操作的結(jié)果可不是聰明的做法。所以它們都會(huì)被封裝成 async Request,并被交給線程池去處理。還記得我們之前舉的餐館工作流程的例子嗎?燒菜是一個(gè)費(fèi)時(shí)間的事情,如果小姐姐拿到我們的訂單,自己跑到后廚去燒菜會(huì)出現(xiàn)什么后果?等她把單子上的菜都燒好再去下一桌點(diǎn)菜的話,對(duì)客人而言就出現(xiàn)了一個(gè) blocking I/O 操作:進(jìn)餐館沒有人接待了。消息隊(duì)列就如同后廚那里的看板。小姐姐只負(fù)責(zé)往看板上添加新的訂單,而訂單的制作交由廚師團(tuán)隊(duì)來完成。
(3)工作線程
工作線程來完成具體的 I/O 請(qǐng)求操作。通常這個(gè)過程藉由 OS 所提供的異步機(jī)制來完成。如 Windows 里面的 IO 完成端口(IOCP)、Linux 里面的異步 IO。如圖 2 所示,當(dāng)工作線程完成了一個(gè)異步請(qǐng)求后,會(huì)把操作結(jié)果放置到一個(gè)消息隊(duì)列里面。從圖中可以看到,主線程運(yùn)行所涉及到的每個(gè) phase 都有各自專屬的消息隊(duì)列。消息隊(duì)列里面有了消息,意味著主線程又需要干活了,干活的過程中會(huì)繼續(xù)產(chǎn)生新的異步請(qǐng)求,工作線程繼續(xù)不知疲倦地搬磚。完美的閉環(huán)。有一種場(chǎng)景圖 2 并沒有畫出來,當(dāng) Node.js 收到來自系統(tǒng)外部的事件如網(wǎng)絡(luò)請(qǐng)求時(shí),工作流程是什么樣子的?到目前為止我們談及的 event 都是由 JS code 主動(dòng)觸發(fā)的,如果我們說這種 event 是由頂向下觸發(fā)的話,網(wǎng)絡(luò)請(qǐng)求這樣的 event 是由底向上觸發(fā)的。聰明的你一定可以在腦袋里大致畫出一條線出來:這條線的起點(diǎn)是位于內(nèi)核的網(wǎng)卡驅(qū)動(dòng),終點(diǎn)是 Node.js 主線程,中間依次經(jīng)過了內(nèi)核協(xié)議棧,Node.js 的消息隊(duì)列。
3、小結(jié)
行文至此,可以看到 Node.js 是一個(gè)完完全全的消息驅(qū)動(dòng)型模型。Node進(jìn)程活著的最大意義是:有各種各樣的 event 以及綁定在 event 上面的 callback 和 data需要它(main thread 和 worker thread)處理。event 的 callback 中也可能會(huì)產(chǎn)生新的異步請(qǐng)求,進(jìn)而產(chǎn)生新的 event 。正是這些源源不斷的 event 驅(qū)動(dòng)著 Node 活下去。如果沒有event需要Node進(jìn)程處理了,它也就沒有存在的必要了。Node.js 還是一個(gè)標(biāo)準(zhǔn)的單進(jìn)行多線程模型。其中主線程用來執(zhí)行我們所寫的 JS code ,而線程池里面的 worker thread 則用來執(zhí)行各種耗時(shí)長的 I/O 操作。這些操作可能會(huì)導(dǎo)致 worker thread 被阻塞掉。worker thread 被阻塞沒有關(guān)系,但主線程被阻塞就不太美麗了。最后再強(qiáng)調(diào)一下:我們所寫的 JS code 是交由 V8 在單線程里面運(yùn)行的,所以盡量不要在 JS code 里面執(zhí)行耗時(shí)長的同步操作。
網(wǎng)站標(biāo)題:圖解Node.js的核心Event-loop
鏈接分享:http://fisionsoft.com.cn/article/djggedh.html


咨詢
建站咨詢
