Node.js Event Loop之Timers, process.nextTick()

前言

Node.js以異步I/O和事件驅動的特性著稱,但異步I/O是怎麼實現的呢?其中核心的一部分就是event loop,下文中內容基原本自於Node.js文檔,有不許確地方請指出.javascript

什麼是Event loop

event loop能讓Node.js的I/O操做表現得無阻塞,儘管JavaScript是單線程的但經過儘量的將操做放到操做系統內核.java

因爲如今大多數內核都是多線程的,它們能夠在後臺執行多個操做. 當這些操做完成時,內核通知Node.js應該把回調函數添加到poll隊列被執行.咱們將在接下來的話題裏詳細討論.node

Event Loop 說明

當Node.js開始時,它將會初始化event loop,處理提供可能形成異步API調用,timers任務,或調用process.nextTick()的腳本(或者將它放到[REPL][]中,這篇文章中將不會討論),而後開始處理event loop.git

下面是一張event loop操做的簡單概覽圖.github

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

注意: 每個方框將被簡稱爲一個event loop的階段.npm

每個階段都有一個回調函數的FIFO隊列被執行.每個階段都有本身特有的方式,一般even loop進入一個給定的階段時,它將執行該階段任何的特定操做,而後執行該階段隊列中的回調函數,直到執行完全部回調或執行了最大回調的次數.當隊列中的回調已被執行完或者到達了限制次數,eventloop將會從下一個階段開始依次執行.api

因爲這些操做可能形成更多的操做,而且在poll階段中產生的新事件被內核推入隊列,因此poll事件能夠被推入隊列當有其它poll事件正在執行時.所以長時間執行回調能夠容許poll階段超過timers設定的時間.詳細內容請看timerspoll章節.瀏覽器

ps: 我的理解-在輪詢階段一個回調執行可能會產生新的事件處理,這些新事件會被推入到輪詢隊列中,因此poll階段能夠一直執行回調,即便timers的回調已到時間應該被執行時.多線程

注意: Windows和Unix/Linux在實現時有一些細微的差別,但那都不是事兒.重點是: 實際上有7或8個步驟,Node.js實際上使用的是它們全部.異步

階段概覽

  • timers: 這個階段執行setTimeout()setInterval()產生的回調.
  • I/O callbacks: 執行大多數的回調,除了close callbacks,timers和setImmediate()的回調.
  • idle, prepare: 僅供內部使用.
  • poll: 獲取新的I/O事件;node會在適當時候在這裏阻塞.
  • check: 執行setImmediate()回調.
  • close callbacks: e.g. socket.on('close', ...).

在每次event loop之間,Node.js會檢查它是否正在等待任何異步I/O或計時器,若是沒有就會徹底關閉.

階段詳情

timers

一個定時器指定的是執行回調函數的閾值,而不是肯定的時間點.定時器的回調將在規定的時間事後運行;然而,操做系統調度或其餘回調函數的運行可能會使執行回調延遲.

注意: 技術上,poll 階段控制了timers被執行.

例如, 你要在100ms的延時後在回調函數而且執行一個耗時95ms的異步讀取文本操做:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(function() {

  const delay = Date.now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
}, 100);


// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(function() {

  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }

});

// 輸出: 105ms have passed since I was scheduled

當event loop進入poll階段時,它是一個空的隊列(fs.readFile()尚未完成),因此它會等待數毫秒等待timers設定時間的到達.直到等待95 ms事後, fs.readFile()完成文件讀取而後它的回調函數會被添加至poll隊列而後執行.當執行完成後隊列中沒有其餘回調,因此event loop會查看定時器設定的時間已經到達而後回撤到timers階段執行timers的回調函數.在例子裏你會發現,從定時器被記錄到執行回調函數耗時105ms.

注意: 爲了防止poll階段阻塞死event loop, [libuv]
(http://libuv.org/) (實現Node.js事件循環的C庫和平臺的全部異步行爲)
也有一個固定最大值(系統依賴).

I/O callbacks

這個階段執行一些系統操做的回調,例如TCP錯誤等類型.例如TCP socket 嘗試鏈接時收到了ECONNREFUSED,一些*nix系統想等待錯誤日誌記錄.這些都將在I/O callbacks階段被推入隊列執行.

poll

poll 階段有兩個主要的功能:

  1. 爲已經到達或超時的定時器執行腳本
  2. 處理在poll隊列中的事件.

當event loop進入poll階段而且沒有timers任務時會執行下面某一條操做:

  • 若是poll隊列不爲空,則event loop會同步的執行回調隊列,直到執行完回調或達到系統最大限制.
  • 若是poll隊列爲空,會執行下面某一條操作:

    • 若是腳本被setImmediate()執行,則event loop會結束 poll階段,繼續向下進入到check階段執行setImmediate()的腳本.
    • 若是腳本不是被setImmediate()執行,event loop會等待回調函數被添加至隊列,而後馬上執行它們.

一旦poll隊列空了,event loop會檢查timers是否有以知足條件的定時器,若是有一個以上知足執行條件的定時器,event loop將會撤回至timers階段去執行定時器的回調函數.

check

這個階段容許馬上執行一個回調在poll階段完成後.若是poll階段已經執行完成或腳本已經使用setImmediate(),event loop 可能就會繼續到check階段而不是等待.

setImmediate()實際是在event loop 獨立階段運行的特殊定時器.它使用了libuv API來使回調函數在poll階段後執行.

一般在代碼執行時,event loop 最終會到達poll階段,等待傳入鏈接,請求等等.然而,若是有一個被setImmediate()執行的回調,poll階段會變得空閒,它將會結束並進入check階段而不是等待新的poll事件.

close callbacks

若是一個socket或者操做被忽然關閉(例如.socket.destroy()),這個close事件將在這個階段被觸發.不然它將會經過process.nextTick()被觸發.

setImmediate() vs setTimeout()

setImmediatesetTimeout() 是很類似的,可是它們的調用方式不一樣致使了會有不一樣的表現.

  • setImmediate() 會中斷poll階段,當即執行..
  • setTimeout() 將在給定的毫秒後執行設定的腳本.

timers的執行順序會根據它們被調用的上下文而變化.若是兩個都在主模塊內被調用,則時序將受到進程的性能的限制(可能受機器上運行的其餘應用程序的影響).

例如,咱們執行下面兩個不在I/O週期內(主模塊)的腳本,這兩個timers的執行順序是不肯定的,它受到進程性能的影響:

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

setImmediate(function immediate() {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

然而,若是你把這兩個調用放到I/O週期內,則immediate的回調總會被先執行:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用setImmediate()setTimeout()的好處是setImmediate()在I/O週期內老是比全部timers先執行,不管有多少timers存在.

process.nextTick()

理解 process.nextTick()

你可能已經注意到process.nextTick()沒有在概覽圖中列出,儘管他是異步API的一部分.這是由於process.nextTick()在技術上不是event loop的一部分.反而nextTickQueue會在當前操做完成後會被執行,不管當前處於event loop的什麼階段.

再看看概覽圖,在給定的階段你任什麼時候候調用process.nextTick(),經過process.nextTick()指定的回調函數都會在event loop繼續執行前被解析.這可能會形成一些很差的狀況,由於它容許你經過遞歸調用process.nextTick()而形成I/O阻塞死,由於它阻止了event loop到達poll階段.

爲何這種操做會被容許呢?

部分緣由是一個API應該是異步事件儘管它可能不是異步的.看看下面代碼片斷:

function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,
                            new TypeError('argument should be string'));
}

代碼裏對參數作了校驗,若是不正確,它將會在回調函數中拋出錯誤.API最近更新,容許傳遞參數給 process.nextTick() ,process.nextTick()能夠接受任何參數,回調函數被當作參數傳遞給回調函數後,你就沒必要使用嵌套函數了.

咱們所作的就是將錯誤回傳給用戶當用戶的其它代碼執行後.經過使用process.nextTick()咱們確保apiCall()執行回調函數在用戶的代碼以後,在event loop運行的階段以前.爲了實現這一點,JS調用的堆棧被容許釋放掉,而後馬上執行提供的回調函數,回調容許用戶遞歸的調用process.nextTick()直到v8限制的調用堆棧最大值.

這種理念可能會致使一些潛在的問題.來看這段代碼:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined

});

bar = 1;

用戶定義了一個有異步標籤的函數someAsyncApiCall(),儘管他的操做是同步的.當它被調用的時候,提供的回調函數在event loop的同一階段中被調用,由於someAsyncApiCall()沒有任何異步操做.因此回調函數嘗試引用bar儘管這個變量在做用域沒有值,由於代碼尚未執行到最後.

經過將回調函數放在process.nextTick()裏,代碼仍然有執行完的能力,容許全部的變量,函數等先被初始化來供回調函數調用.它還有不容許event loop繼續執行的優點.它可能在event loop繼續執行前拋出一個錯誤給用戶頗有用.這裏提供一個使用process.nextTick()的示例:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

這裏有另外一個真實的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

僅當端口可用時端口當即被綁定.因此'listening'的回調函數能當即被調用.問題是那時候不會設置.on('listening').

爲了解決這個問題,'listening'事件被放入nextTick()隊列來容許代碼執行完.這會容許用戶設置他們想要的任何事件處理.

process.nextTick() vs setImmediate()

咱們有兩個直到如今用戶都關心的類似的調用,但他們的名字使人困惑.

  • process.nextTick() 在同一階段當即觸發
  • setImmediate() 在如下迭代器或者event loop的'tick'中觸發

本質上,這兩個名字應該交換.process.nextTick()setImmediate()觸發要快但這是一個不想改變的歷史的命名.作這個改變會破壞npm上大多數包.天天都有新模塊被增長,意味着天天咱們都在等待更多的潛在錯誤發生.當他們困惑時,這個名字就不會被改變.

咱們建議開發者使用setImmediate()由於它更容易被理解(而且它保持了更好的兼容性,例如瀏覽器的JS)

爲何使用process.nextTick()?

有兩個主要緣由:

  1. 容許用戶處理錯誤,清除任何不須要的資源,或者可能在事件循環繼續以前再次嘗試該請求.
  2. 同時有必要容許回調函數執行在調用堆棧釋放以後但在event loop繼續以前.

一個知足用戶期待的簡單例子:

const server = net.createServer();
server.on('connection', function(conn) { });

server.listen(8080);
server.on('listening', function() { });

listen()在event loop開始時執行,可是listening的回調函數被放在一個setImmediate()中.如今除非主機名可用於綁定端口會當即執行.如今爲了event loop繼續執行,它必須進入poll階段,意味着在監聽事件前且沒有觸發容許鏈接事件時沒有接收到請求的可能.

另外一個例子是運行一個函數構造函數,例如,繼承自EventEmitter,而且想要在構造函數中調用一個事件:

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

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

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

你不能在構造函數中當即觸發事件,由於代碼不會執行到用戶爲該事件分配回調函數的地方,因此,在構造函數自己中,你可使用process.nextTick()設置回調函數來在夠咱函數完成後觸發事件.有一個小栗子:

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

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

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

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

部分我的理解

前面基本是基於文檔的翻譯(因爲英文能力問題,不少地方都模模糊糊,甚至是狗屁不通[捂臉]),下面寫一些重點部分的理解

幾個概念

  1. event loop是跑在主進程上的一個while(true) {}循環.
  2. timers階段包括setTimeout(),setInterval()兩個定時器,回調執行時間等於或者晚於定時器設定的時間,由於在poll階段會執行其它回調函數,在空閒時纔回去檢查定時器(event loop的開始和結束時檢查).
  3. 在I/O callback階段,雖然在階段介紹裏說的是執行除timers,Immediate,close以外的全部回調,但後面詳細介紹中又說了,這裏執行的大可能是stream, pipe, tcp, udp通訊錯誤的回調,例如fs產生的回調應該仍是在poll階段執行的.
  4. poll階段應該纔是真正的執行了除timers,Immediate,close外的全部回調.
  5. process.nextTick()沒有在任何一個階段執行,它執行的時間應該是在各個階段切換的中間執行.

幾段代碼

const fs = require('fs');

fs.readFile('../mine.js', () => {
    setTimeout(() => { console.log("setTimeout") }, 0);
    process.nextTick(() => { console.log("process.nextTick") })
    setImmediate(() => { console.log("setImmediate") })
});
/*log -------------------
process.nextTick
setImmediate
setTimeout
*/
  1. 當文件讀取完成後在poll階段執行回調函數
  2. setTimeout添加至timers隊列,解析process.nextTick()回調函數,將setImmediate添加至check隊列
  3. poll隊列爲空,有setImmediate的代碼,繼續向下一個階段.
  4. 在到達check階段前執行process.nextTick()回調函數
  5. check階段執行setImmediate
  6. timers階段執行setTimeout回調
const fs = require('fs');

const start = new Date();
fs.readFile('../mine.js', () => {
    setTimeout(() => { console.log("setTimeout spend: ", new Date() - start) }, 0);
    setImmediate(() => { console.log("setImmediate spend: ", new Date() - start) })
    process.nextTick(() => { console.log("process.nextTick spend: ", new Date() - start) })
});
setTimeout(() => { console.log("setTimeout-main spend: ", new Date() - start) }, 0);
setImmediate(() => { console.log("setImmediate-main spend: ", new Date() - start) })
process.nextTick(() => { console.log("process.nextTick-main spend: ", new Date() - start) })
/* log ----------------
process.nextTick-main spend:  9
setTimeout-main spend:  12
setImmediate-main spend:  13
process.nextTick spend:  14
setImmediate spend:  15
setTimeout spend:  15
*/

這裏沒有搞懂爲何主進程內的setTimeout老是比setImmediate先執行,按文檔所說,兩個應該是不肯定誰先執行.

相關文章
相關標籤/搜索