nodejs 異步I/O和事件驅動

KCCTravis 8年前發布 | 34K 次閱讀 Node.js Node.js 開發

nodejs 異步I/O和事件驅動

注:本文是對眾多博客的學習和總結,可能存在理解錯誤。請帶著懷疑的眼光,同時如果有錯誤希望能指出。

接觸 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/</code></pre>

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/</code></pre>

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/</code></pre>

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 //注意這里的時間晚了好久/</code></pre>

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/</code></pre>

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'); // 運行到這里之前,事件循環是暫停的</code></pre>

  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 這個庫來實現的,包含了 window 和 linux 下面的 異步I/O 實現,博主也沒有研究過這個庫,感興趣的讀者可以移步到 這里

問題答案

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

question 1

為何 timeout1 和 timeout2 的結果會在end后面?

answer 1

因為此時 timeout1 和 timeout2 只是被異步函數推入到了隊列中, 事件循環 還是暫停狀態

question 2

為何 timeout1 和 timeout2 沒有輸出到終端? while(true) 到底阻塞了什么?

answer 2

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

question 3,4

  1. 為什么 timeout1 中回調函數會阻塞 timeout2 中的回調函數的執行?

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

answer 3,4

因為該回調函數執行返回 事件循環 才會繼續執行,回調函數將會阻塞事件循環的運行

question 5

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

answer 5

因為 IO 操作是異步的,會開啟一個新的線程,不會阻塞到 事件循環

參考文獻:

 

來自: https://segmentfault.com/a/1190000005173218

 

 本文由用戶 KCCTravis 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!