nodejs 異步I/O和事件驅動

nodejs 異步I/O和事件驅動

注:本文是對衆多博客的學習和總結,可能存在理解錯誤。請帶着懷疑的眼光,同時若是有錯誤但願能指出。javascript

接觸nodejs有兩個月,對nodejs的兩大特性一直有點模糊,即異步IO事件驅動。經過對《深刻淺出nodejs》和幾篇博客的閱讀之後,有了大體的瞭解,總結一下。html

幾個例子

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


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 1node

爲什麼timeout1timeout2的結果會在end後面?linux


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 2git

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


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中的回調函數的執行?json


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 4segmentfault

和上面的問題同樣,爲什麼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操做是異步的,會開啓一個新的線程,不會阻塞到事件循環


參考文獻:

相關文章
相關標籤/搜索