深入瞭解nodejs的事件迴圈機制

QZhan發表於2018-09-28

一直以來,我寫的的大部分JS程式碼都是在瀏覽器環境下執行,因此也瞭解過瀏覽器的事件迴圈機制,知道有macrotask和microtask的區別。但最近寫node時,發現node的事件迴圈機制和瀏覽器端有很大的不同,特此深入地學習了下。

單執行緒

在傳統web服務中,大多都是使用多執行緒機制來解決併發的問題,原因是I/O事件會阻塞執行緒,而阻塞就意味著要等待。而node的設計是採用了單執行緒的機制,但它為什麼還能承載高併發的請求呢?因為node的單執行緒僅針對主執行緒來說,即每個node程式只有一個主執行緒來執行程式程式碼,但node採用了事件驅動的機制,將耗時阻塞的I/O操作交給執行緒池中的某個執行緒去完成,主執行緒本身只負責不斷地排程,並沒有執行真正的I/O操作。也就是說node實現的是非同步非阻塞式。

事件迴圈機制

node能實現高併發的訣竅就在於事件迴圈機制,這個事件迴圈機制和瀏覽器端的相似但也有很多不同。根據node的官方介紹,node每次事件迴圈機制都包含了6個階段:

  • timers階段:這個階段執行已經到期的timer(setTimeout、setInterval)回撥
  • I/O callbacks階段:執行I/O(例如檔案、網路)的回撥
  • idle, prepare 階段:node內部使用
  • poll階段:獲取新的I/O事件, 適當的條件下node將阻塞在這裡
  • check階段:執行setImmediate回撥
  • close callbacks階段:執行close事件回撥,比如TCP斷開連線

對於日常開發來說,我們比較關注的是timers、I/O callbacks、check階段。node和瀏覽器相比一個明顯的不同就是node在每個階段結束後會去執行所有microtask任務。對於這個特點我們可以做個試驗:

console.log('main');

setImmediate(function() {
    console.log('setImmediate');
});

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});
複製程式碼

程式碼的執行結果是:

  1. main
  2. promise.then
  3. setImemediate

setImmediate 和 process.nextTick

相對於瀏覽器環境,node環境下多出了setImmediate和process.nextTick這兩種非同步操作。setImmediate的回撥函式是被放在check階段執行,即相當於事件迴圈的最後階段了。而process.nextTick會被當做一種microtask,前面提到每個階段結束後都會執行所有microtask任務,所以process.nextTick有種類似於插隊的作用,可以趕在下個階段前執行,但它和promise.then哪個先執行呢?通過一段程式碼來實驗:

console.log('main');

process.nextTick(function() {
    console.log('nextTick')
})

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});
複製程式碼

程式碼的執行結果是:

  1. main
  2. nextTick
  3. promise.then

事實證明,process.nextTick的優先順序會比promise.then高。

process.nextTick的飢餓陷阱

process.nextTick的優勢在於它能夠插入到每個階段之後,在當前階段執行完畢後就能立馬執行。然而它的這個優點也導致瞭如果呼叫不當就容易陷入飢餓陷阱。具體就是當遞迴地呼叫process.nextTick的時候,事件迴圈一直無法進入到下一個階段,導致了後面階段的事件一直無法被執行,產生飢餓問題。

看一個例子就很容易明白

let i = 0;
setImmediate(function() {
    console.log('setImmediate');
});
function callback() {
    console.log('nextTick' + i++);
    if (i < 1000) {
        process.nextTick(callback);
    }
}
callback();
複製程式碼

執行的結果是 nextTick0 nextTick1 nextTick2 ... nextTick999 setImmediate

setImmediate的回撥會一直等待到process.nextTick任務都完成後才能被執行。

小結

1.node的事件迴圈機制和瀏覽器的有所不同,多出了setImmediate 和 process.nextTick這兩種非同步方式。由於process.nextTick會導致I/O飢餓,所以官方也推薦使用setImmediate。 2.node雖然是單執行緒的設計,但它也能實現高併發。原因在於它的主執行緒事件迴圈機制和底層執行緒池的實現。 3.這種機制決定了node比較適合I/O密集型應用,而不適合CPU密集型應用。

相關文章