首頁  >  文章  >  web前端  >  Node.js事件循環(Event Loop)和線程池詳解_node.js

Node.js事件循環(Event Loop)和線程池詳解_node.js

WBOY
WBOY原創
2016-05-16 16:17:231197瀏覽

Node的「事件循環」(Event Loop)是它能夠處理大並發、高吞吐量的核心。這是最神奇的地方,據此Node.js基本上可以理解成“單線程”,同時還允許在後台處理任意的操作。這篇文章將闡明事件循環是如何運作的,你也可以感受到它的神奇。

事件驅動程式設計

理解事件循環,首先要理解事件驅動程式設計(Event Driven Programming)。它出現在1960年。如今,事件驅動程式設計在UI程式設計中大量使用。 JavaScript的一個主要用途是與DOM交互,所以使用基於事件的API是很自然的。

簡單地定義:事件驅動程式設計透過事件或狀態的變化來進行應用程式的流程控制。一般透過事件監聽實現,一旦事件被偵測到(即狀態改變)則呼叫對應的回呼函數。聽起來很熟悉?其實這就是Node.js事件循環的基本運作原理。

如果你熟悉客戶端JavaScript的開發,想想那些.on*()方法,如element.onclick(),他們用來與DOM元素結合,傳遞使用者互動。這個工作模式允許在單一實例上觸發多個事件。 Node.js透過EventEmitter(事件產生器)觸發這種模式,如在伺服器端的Socket和 “http”模組中。可以從一個單一實例觸發一種或一種以上的狀態改變。

另一個常見的模式是表達成功succeed和失敗fail。現在一般有兩種常見的實作方式。首先是將「Error異常」傳入回調,一般會作為第一個參數傳遞給回調函數。第二種即使用Promises設計模式,已經加入了ES6。註* Promise模式採用類似jQuery的函數鍊式書寫方式,以避免深層的回呼函數嵌套,如:

複製程式碼 程式碼如下:

$.getJSON('/getUser').done(successHandler).fail(failHandler)

“fs”(filesystem)模組大多採用往回調中傳入異常的風格。在技​​術上觸發某些調用,例如fs.readFile()附加事件,但此API只是為了提醒用戶,用來表達操作成功或失敗。選擇這樣的API是出於架構的考慮,而非技術的限制。

一個常見的誤解是,事件發生器(event emitters)在觸發事件時也是天生非同步的,但這是不正確的。下面是一個簡單的程式碼片段,以證明這一點。

複製程式碼 程式碼如下:

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

MyEmitter.prototype.doStuff = function doStuff() {
  console.log('before')
  emitter.emit('fire')
  console.log('after')}
};

var me = new MyEmitter();
me.on('fire', function() {
  console.log('emit fired');
});

me.doStuff();
// 輸出:
// before
// emit fired
// after

註* 如果 emitter.emit 是非同步的,則輸出應該為
// before
// after
// emit fired


EventEmitter經常表現地很非同步,因為它經常用於通知需要非同步完成的操作,但EventEmitter API本身是完全同步的。監聽函數內部可以以非同步執行,但請注意,所有的監聽函數會依照被新增的順序同步執行。

機制概述與執行緒池

Node本身依賴多個函式庫。其中之一是libuv,神奇的處理非同步事件佇列和執行的函式庫。

Node利用盡可能多的利用作業系統核心實作現有的功能。像產生回應請求(request),轉發連線(connections)並委託給系統處理。例如,傳入的連接會透過作業系統進行佇列管理,直到它們可以由Node處理。

您可能聽說過,Node有一個線程池,你可能會疑惑:「如果Node會按次序處理任務,為什麼還需要一個線程池?」這是因為在核心中,不是所有任務都是按異步執行的。在這種情況下,Node.JS必須能在操作時將執行緒鎖定一段時間,以便它可以繼續執行事件循環而不會被阻塞。

下面是一個簡單的範例圖,來表示他內部的運作機制:


            ┌───────────────────────┐
╭-►│         timers                         │         └─-────────┬───────────┘
 │         ┌───────────┴───────────┐
 │         │   pending callbacks                     │         └─-────────┬───────────┘          ┌──────────────┐
 │         ┌─------┴─-            │             │◄-┤ connections,               │
 │         └─-────────┬───────────┘          │ >  │         ┌───────────┴───────────┐          └──────────────┘
╰─-┤      setImmediate                                       └───────────────────────┘

關於事件循環的內部運作機制,有一些理解困難的地方:

所有回呼都會經由process.nextTick(),在事件循環(例如,定時器)一個階段的結束並轉換到下一階段之前預先設定。這就會避免潛在的遞歸呼叫process.nextTick(),而造成的無限迴圈。
“Pending callbacks(待回調)”,是回呼佇列中不會被任何其他事件循環週期處理(例如,傳遞給fs.write)的回呼。

Event Emitter 和 Event Loop


透過建立EventEmitter,可簡化與事件迴圈的互動。它是一個通用的封裝,可以讓你更容易建立基於事件的API。關於這兩者如何互動往往會讓開發者感到混亂。

下面的例子表明,忘記了事件是同步觸發的,可能導致事件被錯過。

複製程式碼 程式碼如下:

// v0.10以後,不再需要require('events').EventEmitter
var EventEmitter = require('events');
var util = require('util');

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

  doFirstThing();
  this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);

var mt = new MyThing();

mt.on('thing1', function onThing1() {
  // 抱歉,這事件永遠不會發生
});


上面的'thing1'事件,永遠不會被MyThing()捕獲,因為MyThing()必須在實例化後才能偵聽事件。下面的是一個簡單的解決方法,不必添加任何額外的閉包:
複製程式碼 程式碼如下:

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

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

  doFirstThing();
  setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);

function emitThing1(self) {
  self.emit('thing1');
}

var mt = new MyThing();

mt.on('thing1', function onThing1() {
  // 執行了
});

下面的方案也可以工作,不過要損失一些性能:

複製程式碼 程式碼如下:

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

  doFirstThing();
  // 使用 Function#bind() 會損失效能
  setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);


另一個問題是觸發Error(異常)。要找出您應用程式中的問題已經很難了,但沒了呼叫堆疊(註* e.stack),則幾乎不可能調試。當Error被遠端的非同步請求呼叫堆疊將會遺失。有兩個可行的解決方案:同步觸發或確保Error跟其他重要訊息一起傳入。下面的範例示範了這兩種解決方案:
複製程式碼 程式碼如下:

MyThing.prototype.foo = function foo() {
  // 這個 error 會被非同步觸發
  var er = doFirstThing();
  if (er) {
    // 在觸發時,需要建立一個新的保留現場呼叫堆疊資訊的error
    setImmediate(emitError, this, new Error('Bad stuff'));
    return;
  }

  // 觸發error,馬上處理(同步)
  var er = doSecondThing();
  if (er) {
    this.emit('error', 'More bad stuff');
    return;
  }
}


審時度勢。當error被觸發時,是有可能被立即處理的。或者,它可能是一些瑣碎的,可以很容易處理,或在以後再處理的異常。另外透過一個建構函數,傳遞Error也不是個好主意,因為建構出來的物件實例很有可能是不完整的。剛才直接拋出Error的情況是例外。

結束語

這篇文章比較淺顯地探討了有關事件循環的內部運作機制和技術細節。都是經過深思熟慮的。另一篇文章會討論事件循環與系統核心的交互,並展現NodeJS非同步運行的魔力。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn