【Node.js】理解事件循環機制

前沿

Node.js 是基於V8引擎的javascript運行環境. Node.js具備事件驅動, 非阻塞I/O等特色. 結合Node API, Node.js 具備網絡編程, 文件系統等服務端的功能, Node.js用libuv庫進行異步事件處理.javascript

線程

Node.js的單線程含義, 實際上說的是執行同步代碼的主線程. 一個Node程序的啓動, 不止是分配了一個線程,而是咱們只能在一個線程執行代碼. 當出現I/O資源調用, TCP鏈接等外部資源申請的時候, 不會阻塞主線程, 而是委託給I/O線程進行處理,而且進入等待隊列. 一旦主線程執行完成,將會消費事件隊列(Event Queue). 由於只有一個主線程, 只佔用CPU內核處理邏輯計算, 所以不適合在CPU密集型進行使用.html

Node核心

注意,上圖的EVENT_QUEUE 給人看起來是隻有一個隊列, 根據Node.js官方介紹, EventLoop有6個階段, 同時每一個階段都有對應的一個先進先出的回調隊列. java

什麼是事件循環(EventLoop) ?

In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. -- from wikinode

大概含義: EventLoop 是一種經常使用的機制,經過對內部或外部的事件提供者發出請求, 如文件讀寫, 網絡鏈接 等異步操做, 完成後調用事件處理程序. 整個過程都是異步階段git

Node.js的事件循環機制

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop. -- from node.js docgithub

大體含義: 當Node.js 啓動, 就會初始化一個 event loop, 處理腳本時, 可能會發生異步API行爲調用, 使用定時器任務或者nexTick, 處理完成後進入事件循環處理過程編程

事件循環階段

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

每個階段都有一個FIFO的callbacks隊列, 每一個階段都有本身的事件處理方式. 當事件循環進入某個階段時, 將會在該階段內執行回調,直到隊列耗盡或者回調的最大數量已執行, 那麼將進入下一個處理階段. 網絡

  • timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預約的callback;
  • I/O callbacks 階段: 執行除了close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks以外的callbacks; (目前這個階段)
  • idle, prepare 階段: 僅node內部使用;
  • poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裏;
  • check 階段: 執行setImmediate() 設定的callbacks;
  • close callbacks 階段: 好比socket.on(‘close’, callback)的callback會在這個階段執行.

下面是摘抄creeperyang 對上面6個階段的 (原文翻譯)異步

timers階段

一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定時間事後,timers會盡量早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。socket

注意:技術上來講,poll 階段控制 timers 何時執行。

注意:這個下限時間有個範圍:[1, 2147483647],若是設定的時間不在這個範圍,將被設置爲1。

I/O callbacks階段

這個階段執行一些系統操做的回調。好比TCP錯誤,如一個TCP socket在想要鏈接時收到ECONNREFUSED,
類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行.
名字會讓人誤解爲執行I/O回調處理程序, 實際上I/O回調會由poll階段處理.

poll階段

poll 階段有兩個主要功能:

執行下限時間已經達到的timers的回調,而後
處理 poll 隊列裏的事件。
當event loop進入 poll 階段,而且 沒有設定的timers(there are no timers scheduled),會發生下面兩件事之一:

若是 poll 隊列不空,event loop會遍歷隊列並同步執行回調,直到隊列清空或執行的回調數到達系統上限;

若是 poll 隊列爲空,則發生如下兩件事之一:

  1. 若是代碼已經被setImmediate()設定了回調, event loop將結束 poll 階段進入 check 階段來執行 check 隊列(裏的回調)。
  2. 若是代碼沒有被setImmediate()設定回調,event loop將阻塞在該階段等待回調被加入 poll 隊列,並當即執行。

可是,當event loop進入 poll 階段,而且 有設定的timers,一旦 poll 隊列爲空(poll 階段空閒狀態):

  1. event loop將檢查timers,若是有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段,並執行 timer 隊列。

check階段

這個階段容許在 poll 階段結束後當即執行回調。若是 poll 階段空閒,而且有被setImmediate()設定的回調,event loop會轉到 check 階段而不是繼續等待。

setImmediate()其實是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv的API
來設定在 poll 階段結束後當即執行回調。

一般上來說,隨着代碼執行,event loop終將進入 poll 階段,在這個階段等待 incoming connection, request 等等。可是,只要有被setImmediate()設定了回調,一旦 poll 階段空閒,那麼程序將結束 poll 階段並進入 check 階段,而不是繼續等待 poll 事件們 (poll events)。

close callbacks 階段

若是一個 socket 或 handle 被忽然關掉(好比 socket.destroy()),close事件將在這個階段被觸發,不然將經過process.nextTick()觸發

簡單的 EventLoop

const fs = require('fs');
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function asyncOperation (callback) {
  fs.readFile(__dirname + '/' + __filename, callback);
}

const lastTime = Date.now();

setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

process.nextTick(() => {
  // 進入event loop
  // timers階段以前執行
  wait(20);
  asyncOperation(() => {
    console.log('poll');
  });  
});

/**
 * result:
 * timers 21ms
 * poll
 */

爲了讓setTimeout優先於fs.readFile 回調, 執行了process.nextTick, 表示在進入 timers階段前, 等待20ms後執行文件讀取.

nextTick 與 setImmediate

process.nextTick 不屬於事件循環的任何一個階段,它屬於該階段與下階段之間的過渡, 即本階段執行結束, 進入下一個階段前, 所要執行的回調。有給人一種插隊的感受.

setImmediate的回調處於check階段, 當poll階段的隊列爲空, 且check階段的事件隊列存在的時候,切換到check階段執行.

nextTick 遞歸的危害
因爲nextTick具備插隊的機制,nextTick的遞歸會讓事件循環機制沒法進入下一個階段. 致使I/O處理完成或者定時任務超時後仍然沒法執行, 致使了其它事件處理程序處於飢餓狀態. 爲了防止遞歸產生的問題, Node.js 提供了一個 process.maxTickDepth (默認 1000)。

遞歸nextTick

const fs = require('fs');
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function nextTick () {
  process.nextTick(() => {
    wait(20);
    nextTick();
  });
}

const lastTime = Date.now();

setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

nextTick();

此時永遠沒法跳到timer階段, 由於在進入timers階段前有不斷的nextTick插入執行. 除非執行了1000次到了執行上限.

setImmediate
若是在一個I/O週期內進行調度,setImmediate()將始終在任何定時器以前執行.

setTimeout 與 setImmediate

  • setImmediate()被設計在 poll 階段結束後當即執行回調;
  • setTimeout()被設計在指定下限時間到達後執行回調;

無 I/O 處理狀況下

setTimeout(function timeout () {
  console.log('timeout');
},0);

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

輸出結果是 不肯定 的!
setTimeout(fn, 0) 具備幾毫秒的不肯定性. 沒法保證進入timers階段, 定時器可以當即執行處理程序.

在I/O事件處理程序下

var fs = require('fs')

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

此時 setImmediate 優先於 setTimeout 執行,由於 poll階段執行完成後 進入 check階段. timers階段處於下一個事件循環階段了.

相關文章

相關文章
相關標籤/搜索