1. 程式人生 > >一、NodeJS事件驅動模型

一、NodeJS事件驅動模型

接觸nodejs有兩個月,對nodejs的兩大特性一直有點模糊,即非同步IO事件驅動。通過對《深入淺出nodejs》和幾篇部落格的閱讀以後,有了大致的瞭解,總結一下。

幾個例子

在開始之前,先來看幾個簡單例子,這也是我在使用nodejs時候遇到的幾個比較困惑的例子。


example 1

var fs = require("fs");
var debug = require('debug')('example1');

debug("begin");

setTimeout(function(){
    debug("timeout1");
});

setTimeout(function(){
    debug("timeout2");
});

debug('end');
/** 執行結果
Sat, 21 May 2016 08:41:09 GMT example1 begin
Sat, 21 May 2016 08:41:09 GMT example1 end
Sat, 21 May 2016 08:41:09 GMT example1 timeout1
Sat, 21 May 2016 08:41:09 GMT example1 timeout2
*/

question 1

為何timeout1timeout2的結果會在end後面?


example 2

var fs = require("fs");
var debug = require('debug')('example2');

debug("begin");

setTimeout(function(){
    debug("timeout1");
});

setTimeout(function(){
    debug("timeout2");
});

debug('end');

while(true);
/**  執行結果
Sat, 21 May 2016 08:45:47 GMT example2 begin
Sat, 21 May 2016 08:45:47 GMT example2 end
*/

question 2

為何timeout1timeout2沒有輸出到終端?while(true)到底阻塞了什麼?


example 3

var fs = require("fs");
var debug = require('debug')('example3');

debug("begin");

setTimeout(function(){
    debug("timeout1");
    while (true);
});

setTimeout(function(){
    debug("timeout2");
});

debug('end');
/**  執行結果
Sat, 21 May 2016 08:49:12 GMT example3 begin
Sat, 21 May 2016 08:49:12 GMT example3 end
Sat, 21 May 2016 08:49:12 GMT example3 timeout1
*/

question 3

為什麼timeout1中回撥函式會阻塞timeout2中的回撥函式的執行?


example 4

var fs = require("fs");
var debug = require('debug')('example4');

debug("begin");

setTimeout(function(){
    debug("timeout1");
    /**
     * 模擬計算密集
     */
    for(var i = 0 ; i < 1000000 ; ++i){
        for(var j = 0 ; j < 100000 ; ++j);
    }
});

setTimeout(function(){
    debug("timeout2");
});

debug('end');
/**
Sat, 21 May 2016 08:53:27 GMT example4 begin
Sat, 21 May 2016 08:53:27 GMT example4 end
Sat, 21 May 2016 08:53:27 GMT example4 timeout1
Sat, 21 May 2016 08:54:09 GMT example4 timeout2  //注意這裡的時間晚了好久
*/

question 4

和上面的問題一樣,為何timeout1的計算密集型工作將會阻塞timeout2的回撥函式的執行?


example 5

var fs = require("fs");
var debug = require('debug')('example5');

debug("begin");

fs.readFile('package.json','utf-8',function(err,data){
    if(err)  
        debug(err);
    else
        debug("get file content");
});

setTimeout(function(){
    debug("timeout2");
});

debug('end');
/** 執行結果
Sat, 21 May 2016 08:59:14 GMT example5 begin
Sat, 21 May 2016 08:59:14 GMT example5 end
Sat, 21 May 2016 08:59:14 GMT example5 timeout2
Sat, 21 May 2016 08:59:14 GMT example5 get file content
*/

question 5

為何讀取檔案的IO操作不會阻塞timeout2的執行?


接下來我們就帶著上面幾個疑惑去理解nodejs中的非同步IO事件驅動是如何工作的。

非同步IO(asynchronous I/O)

首先來理解幾個容易混淆的概念,阻塞IO(blocking I/O)非阻塞IO(non-blocking I/O)同步IO(synchronous I/O)和非同步IO(synchronous I/O)

博主一直天真的以為非阻塞I/O就是非同步I/O T_T,apue一直沒有讀懂。

阻塞I/O 和 非阻塞I/O

簡單來說,阻塞I/O就是當用戶發一個讀取檔案描述符的操作的時候,程序就會被阻塞,直到要讀取的資料全部準備好返回給使用者,這時候程序才會解除block的狀態。

非阻塞I/O呢,就與上面的情況相反,使用者發起一個讀取檔案描述符操作的時,函式立即返回,不作任何等待,程序繼續執行。但是程式如何知道要讀取的資料已經準備好了呢?最簡單的方法就是輪詢。

除此之外,還有一種叫做IO多路複用的模式,就是用一個阻塞函式同時監聽多個檔案描述符,當其中有一個檔案描述符準備好了,就馬上返回,在linux下,select,poll,epoll都提供了IO多路複用的功能。

同步I/O 和 非同步I/O

那麼同步I/O非同步I/O又有什麼區別麼?是不是隻要做到非阻塞IO就可以實現非同步I/O呢?

其實不然。

  • 同步I/O(synchronous I/O)I/O operation的時候會將process阻塞,所以阻塞I/O非阻塞I/OIO多路複用I/O都是同步I/O

  • 非同步I/O(asynchronous I/O)I/O opertaion的時候將不會造成任何的阻塞。

非阻塞I/O都不阻塞了為什麼不是非同步I/O呢?其實當非阻塞I/O準備好資料以後還是要阻塞住程序去核心拿資料的。所以算不上非同步I/O

這裡借一張圖(圖來自這裡)來說明他們之間的區別

![Alt text\][1]

更多IO更多的詳細內容可以在這裡找到:

事件驅動

事件驅動(event-driven)nodejs中的第二大特性。何為事件驅動呢?簡單來說,就是通過監聽事件的狀態變化來做出相應的操作。比如讀取一個檔案,檔案讀取完畢,或者檔案讀取錯誤,那麼就觸發對應的狀態,然後呼叫對應的回掉函式來進行處理。

執行緒驅動和事件驅動

那麼執行緒驅動程式設計和事件驅動程式設計之間的區別是什麼呢?

  • 執行緒驅動就是當收到一個請求的時候,將會為該請求開一個新的執行緒來處理請求。一般存在一個執行緒池,執行緒池中有空閒的執行緒,會從執行緒池中拿取執行緒來進行處理,如果執行緒池中沒有空閒的執行緒,新來的請求將會進入佇列排隊,直到執行緒池中空閒執行緒。

  • 事件驅動就是當進來一個新的請求的時,請求將會被壓入佇列中,然後通過一個迴圈來檢測佇列中的事件狀態變化,如果檢測到有狀態變化的事件,那麼就執行該事件對應的處理程式碼,一般都是回撥函式。

對於事件驅動程式設計來說,如果某個時間的回撥函式是計算密集型,或者是阻塞I/O,那麼這個回撥函式將會阻塞後面所有事件回撥函式的執行。這一點尤為重要。

nodejs的事件驅動和非同步I/O

事件驅動模型

上面介紹了那麼多的概念,現在我們來看看nodejs中的事件驅動非同步I/O是如何實現的.

nodejs單執行緒(single thread)執行的,通過一個事件迴圈(event-loop)來迴圈取出訊息佇列(event-queue)中的訊息進行處理,處理過程基本上就是去呼叫該訊息對應的回撥函式。訊息佇列就是當一個事件狀態發生變化時,就將一個訊息壓入佇列中。

nodejs的時間驅動模型一般要注意下面幾個點:

  • 因為是單執行緒的,所以當順序執行js檔案中的程式碼的時候,事件迴圈是被暫停的。

  • js檔案執行完以後,事件迴圈開始執行,並從訊息佇列中取出訊息,開始執行回撥函式

  • 因為是單執行緒的,所以當回撥函式被執行的時候,事件迴圈是被暫停的

  • 當涉及到I/O操作的時候,nodejs會開一個獨立的執行緒來進行非同步I/O操作,操作結束以後將訊息壓入訊息佇列

下面我們從一個簡單的js檔案入手,來看看 nodejs是如何執行的。

var fs = require("fs");
var debug = require('debug')('example1');

debug("begin");

fs.readFile('package.json','utf-8',function(err,data){
    if(err)  
        debug(err);
    else
        debug("get file content");
});

setTimeout(function(){
    debug("timeout2");
});

debug('end'); // 執行到這裡之前,事件迴圈是暫停的
  1. 同步執行debug("begin")

  2. 非同步呼叫fs.readFile(),此時會開一個新的執行緒去進行非同步I/O操作

  3. 非同步呼叫setTimeout(),馬上將超時資訊壓入到訊息佇列

  4. 同步呼叫debug("end")

  5. 開啟事件迴圈,彈出訊息佇列中的資訊(目前是超時資訊)

  6. 然後執行資訊對應的回撥函式(事件迴圈又被暫停)

  7. 回撥函式執行結束後,開始事件迴圈(目前訊息佇列中沒有任何東西,檔案還沒讀完)

  8. 非同步I/O讀取檔案完畢,將訊息壓入訊息佇列(訊息中含有檔案內容或者是出錯資訊)

  9. 事件迴圈取得訊息,執行回撥

  10. 程式退出。

這裡借一張圖來說明nodejs的事件驅動模型(圖來自這裡
![這裡寫圖片描述\][2]

這裡最後要說的一點就是如何手動將一個函式推入佇列,nodejs為我們提供了幾個比較方便的方法:

  • setTimeout()

  • process.nextTick()

  • setImmediate()

非同步I/O

nodejs中的非同步I/O的操作是通過libuv這個庫來實現的,包含了windowlinux下面的非同步I/O實現,博主也沒有研究過這個庫,感興趣的讀者可以移步到這裡

問題答案

好,到目前為止,已經可以回答上面的問題了


question 1

為何timeout1timeout2的結果會在end後面?

answer 1

因為此時timeout1timeout2只是被非同步函式推入到了佇列中,事件迴圈還是暫停狀態


question 2

為何timeout1timeout2沒有輸出到終端?while(true)到底阻塞了什麼?

answer 2

因為此處直接阻塞了事件迴圈,還沒開始,就已經被阻塞了


question 3,4

  1. 為什麼timeout1中回撥函式會阻塞timeout2中的回撥函式的執行?

  2. 為何timeout1的計算密集型工作將會阻塞timeout2的回撥函式的執行?

answer 3,4

因為該回調函式執行返回事件迴圈才會繼續執行,回撥函式將會阻塞事件迴圈的執行


question 5

為何讀取檔案的IO操作不會阻塞timeout2的執行?

answer 5

因為IO操作是非同步的,會開啟一個新的執行緒,不會阻塞到事件迴圈


參考文獻: