經過源碼解析 Node.js 中高效的 timer

在 Node.js 中,許許多多的異步操做,都須要來一個兜底的超時,這時,就輪到 timer 登場了。因爲須要使用它的地方是那麼的多,並且都是基礎的功能模塊,因此,對於它性能的要求,天然是十分高的。總結來講,要求有:node

  • 更快的添加操做。git

  • 更快的移除操做。github

  • 更快的超時觸發。數據結構

接下來就讓咱們跟着 Node.js 項目中的 lib/timer.jslib/internal/linklist.js 來探究它具體的實現。app

更快的添加 / 移除操做

說到添加和移除都十分高效的數據結構,第一個映入腦簾的,天然就是鏈表啦。是的,Node.js 就是使用了雙向鏈表,來將 timer 的插入和移除操做的時間複雜度都降至 O(1) 。雙向鏈表的具體實現便在 lib/internal/linklist.js 中:dom

// lib/internal/linklist.js
'use strict';
function init(list) {
  list._idleNext = list;
  list._idlePrev = list;
}
exports.init = init;

function peek(list) {
  if (list._idlePrev == list) return null;
  return list._idlePrev;
}
exports.peek = peek;

function shift(list) {
  var first = list._idlePrev;
  remove(first);
  return first;
}
exports.shift = shift;

function remove(item) {
  if (item._idleNext) {
    item._idleNext._idlePrev = item._idlePrev;
  }

  if (item._idlePrev) {
    item._idlePrev._idleNext = item._idleNext;
  }

  item._idleNext = null;
  item._idlePrev = null;
}
exports.remove = remove;

function append(list, item) {
  remove(item);
  item._idleNext = list._idleNext;
  list._idleNext._idlePrev = item;
  item._idlePrev = list;
  list._idleNext = item;
}
exports.append = append;

function isEmpty(list) {
  return list._idleNext === list;
}
exports.isEmpty = isEmpty;

能夠看到,都是些修改鏈表中指針的操做,都十分高效。異步

更快的超時觸發

鏈表的缺點,天然是它的查找時間,對於一個無序的鏈表來講,查找時間須要 O(n) ,可是,只要基於一個大前提,那麼咱們的實現就並不須要使用到鏈表的查詢,這也是更高效的超時觸發的基礎所在,那就是,對於同一延遲的 timers ,後添加的必定比先添加的晚觸發。因此,源碼的具體作法就是,對於同一延遲的全部 timers ,所有都維護在同一個雙向鏈表中,後來的,就不斷往鏈表末尾追加,而且這條鏈表實際上共享同一個定時器 。這個定時器會在當次超時觸發時,動態計算下一次的觸發時間點。全部的鏈表,都保存在一個對象 map 中。如此一來,既作到了定時器的複用優化,又對鏈表結構進行了揚長避短。函數

讓咱們先以 setTimeout 爲例看看具體代碼,首先是插入:性能

// lib/timer.js
// ...
const refedLists = {};
const unrefedLists = {};

exports.setTimeout = function(callback, after) {
  // ... 
  var timer = new Timeout(after);
  var length = arguments.length;
  var ontimeout = callback;
  // ...
  timer._onTimeout = ontimeout;
  
  active(timer);
  return timer;
};

const active = exports.active = function(item) {
  insert(item, false);
};

function insert(item, unrefed) {
  const msecs = item._idleTimeout;
  if (msecs < 0 || msecs === undefined) return;

  item._idleStart = TimerWrap.now();

  var list = lists[msecs];
  if (!list) {
    // ...
    list = new TimersList(msecs, unrefed);
    L.init(list);
    list._timer._list = list;

    if (unrefed === true) list._timer.unref();
    list._timer.start(msecs, 0);

    lists[msecs] = list;
    list._timer[kOnTimeout] = listOnTimeout;
  }

  L.append(list, item);
  assert(!L.isEmpty(list));
}

即檢查當前在對象 map 中,是否存在該超時時間(msecs)的雙向鏈表,若無,則新建一條。你應該已經看出,超時觸發時具體的處理邏輯,就在 listOnTimeout 函數中:優化

// lib/timer.js
// ...
function listOnTimeout() {
  var list = this._list;
  var msecs = list.msecs;

  var now = TimerWrap.now();

  var diff, timer;
  while (timer = L.peek(list)) {
    diff = now - timer._idleStart;

    if (diff < msecs) {
      this.start(msecs - diff, 0);
      return;
    }
    L.remove(timer);
    // ...
    tryOnTimeout(timer, list);
    // ...
  }

  this.close();
  // ...
}

即不斷從鏈表頭取出封裝好的包含了註冊時間點處理函數的對象,而後挨個執行,直到計算出的超時時間點已經超過當前時間點。

舉個圖例,在時間點 10,100,400 時分別註冊了三個超時時間爲 1000 的 timer,在時間點 300 註冊了一個超時時間爲 3000 的 timer,即在時間點 500 時,對象 map 的結構即爲:

3.pic.jpg

隨後在時間點 1200 觸發了超時事件,並在時間點 1300 執行完畢,彼時對象 map 的結構即爲:

4.pic.jpg

setInterval 和 setImmediate

setInterval 的實現整體和 setTimeout 很類似,區別在於對註冊的回調函數進行了封裝,在鏈表的尾部從新插入:

// lib/timer.js
// ...

function wrapper() {
  timer._repeat(); // 執行傳入的回調函數

  if (!timer._repeat)
    return;

  // ...
  timer._idleTimeout = repeat;
  active(timer);
}

setImmediatesetTimeout 實現上的主要區別則在於,它會一次性將鏈表中註冊的,都執行完:

// lib/timer.js
// ...
function processImmediate() {
  var queue = immediateQueue;
  var domain, immediate;

  immediateQueue = {};
  L.init(immediateQueue);

  while (L.isEmpty(queue) === false) {
    immediate = L.shift(queue);
    // ...
    tryOnImmediate(immediate, queue);
    // ...
  }

  if (L.isEmpty(immediateQueue)) {
    process._needImmediateCallback = false;
  }
}

因此做爲功能相似的 process.nextTicksetImmediate ,在功能層面上看,每次事件循環,它們都會將存儲的回調都執行完,但 process.nextTick 中的存儲的回調,會先於 setImmediate 中的執行:

'use strict'
const print = (i) => () => console.log(i)

process.nextTick(print(1))
process.nextTick(print(2))

setImmediate(() => {
  print(3)()
  setImmediate(print(6))
  process.nextTick(print(5))
})
setImmediate(print(4))

console.log('發車')

// 發車
// 1
// 2
// 3
// 4
// 5
// 6

最後

參考:

相關文章
相關標籤/搜索