一、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
為何
timeout1
和timeout2
的結果會在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
為何
timeout1
和timeout2
沒有輸出到終端?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/O
,IO多路複用I/O
都是同步I/O
。 -
非同步I/O(asynchronous I/O)
做I/O opertaion
的時候將不會造成任何的阻塞。
非阻塞I/O
都不阻塞了為什麼不是非同步I/O
呢?其實當非阻塞I/O
準備好資料以後還是要阻塞住程序去核心拿資料的。所以算不上非同步I/O
。
這裡借一張圖(圖來自這裡)來說明他們之間的區別
][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'); // 執行到這裡之前,事件迴圈是暫停的
-
同步執行
debug("begin")
-
非同步呼叫
fs.readFile()
,此時會開一個新的執行緒去進行非同步I/O
操作 -
非同步呼叫
setTimeout()
,馬上將超時資訊壓入到訊息佇列中 -
同步呼叫
debug("end")
-
開啟事件迴圈,彈出訊息佇列中的資訊(目前是超時資訊)
-
然後執行資訊對應的回撥函式(事件迴圈又被暫停)
-
回撥函式執行結束後,開始事件迴圈(目前訊息佇列中沒有任何東西,檔案還沒讀完)
-
非同步I/O
讀取檔案完畢,將訊息壓入訊息佇列(訊息中含有檔案內容或者是出錯資訊) -
事件迴圈取得訊息,執行回撥
-
程式退出。
這裡借一張圖來說明nodejs
的事件驅動模型(圖來自這裡)
][2]
這裡最後要說的一點就是如何手動將一個函式推入佇列,nodejs
為我們提供了幾個比較方便的方法:
-
setTimeout()
-
process.nextTick()
-
setImmediate()
非同步I/O
nodejs
中的非同步I/O
的操作是通過libuv
這個庫來實現的,包含了window
和linux
下面的非同步I/O
實現,博主也沒有研究過這個庫,感興趣的讀者可以移步到這裡
問題答案
好,到目前為止,已經可以回答上面的問題了
question 1
為何
timeout1
和timeout2
的結果會在end後面?
answer 1
因為此時
timeout1
和timeout2
只是被非同步函式推入到了佇列中,事件迴圈還是暫停狀態
question 2
為何
timeout1
和timeout2
沒有輸出到終端?while(true)
到底阻塞了什麼?
answer 2
因為此處直接阻塞了事件迴圈,還沒開始,就已經被阻塞了
question 3,4
為什麼
timeout1
中回撥函式會阻塞timeout2
中的回撥函式的執行?為何
timeout1
的計算密集型工作將會阻塞timeout2
的回撥函式的執行?
answer 3,4
因為該回調函式執行返回事件迴圈才會繼續執行,回撥函式將會阻塞事件迴圈的執行
question 5
為何讀取檔案的IO操作不會阻塞
timeout2
的執行?
answer 5
因為
IO
操作是非同步的,會開啟一個新的執行緒,不會阻塞到事件迴圈
參考文獻: