nodejs事件輪詢詳述

轉載自:http://www.cnblogs.com/xiaozhi_5638/p/4816265.html?utm_source=tuicool&utm_medium=referral

目錄

 

概述

關於nodejs的介紹網上資料非常多,最近由於在整理一些函數式編程的資料時,多次遇到nodejs有關的內容。所以就打算專門寫一篇文章總結一下nodejs相關知識,包括“說它單線程是什麼意思”、“非阻塞又是指什麼”以及最重要的是它的“事件輪詢”的實現機制。

本文不介紹nodejs的優缺點(適用場合)、nodejs環境怎樣搭建以及一些nodejs庫的使用等等這些基礎知識。

 

nodejs特點

網上任何一篇關於nodejs的介紹中均會提及到nodejs兩個主要特點:單線程、非阻塞。但是據我所瞭解到的,大部分介紹一帶而過,並沒有詳細地、系統性地去說明它們到底是怎麼回事。下面我依次盡我所能詳細地說一下我對以上兩者的理解。

非阻塞

我們先來看一段.NET中異步編程的代碼:

using(FileStream fs = new FileStream("hello.txt", FileMode.Open))
{
    byte[] data = new byte[fs.Length];
    fs.BeginRead(data, 0, fs.Length, new AsyncCallback(onRead), null);
    Console.WriteLine("the end");
}

如上代碼所示,由於FileStream.BeginRead是一個異步方法,所以不管hello.txt文件有多大,FileStream.BeginRead方法的調用並不會阻塞調用線程,Console.WriteLine方法立馬便可執行。同理,如果在nodejs中所有的方法都是“異步方法”,那麼在nodejs中任何方法的調用均不會阻塞調用線程,實質上,nodejs中大部分庫方法確實是這樣的。這就是爲什麼我們會說nodejs中代碼是非阻塞的。

單線程

對這個概念有誤解的人非常之多,以爲nodejs程序中就一個線程,然後有很多人會問:既然只有一個線程,那麼怎麼並行處理多個任務呢?

其實這裏說的單線程並不是指nodejs程序中只有一個線程存在,我個人感覺官方給出“單線程”說法本身就具有誤導性,所以也怪不得大部分初學者。那麼“單線程”到底什麼意思呢?其實這裏的“單線程”指的是我們(開發者)編寫的代碼只能運行在一個線程當中(可以稱之爲主線程吧),就像我們在Windows桌面程序開發中一樣,編寫的所有界面代碼均運行在UI線程之中。

那麼還是剛纔那個問題,所有編寫的代碼均運行在一個線程中,那麼怎樣去並行處理任務呢?這個就要想到前面介紹的“異步方法”了,沒錯,雖然開發者編寫的所有代碼均運行在一個線程中,但是我們可以在這個線程中調用異步方法啊,而異步方法內部實現過程當然要採用多線程了。就像下圖:

如上圖所示,nodejs中的單線程指的是圖中的主線程,該主線程中包含一個循環結構,維持整個程序持續運轉。

注:該循環結構也稱之爲“泵”結構,是每個系統必備的結構。具體可以參見我之前的一篇博客《動力之源:代碼中的泵》

因此我們可以說,在nodejs中寫的代碼(包括回調方法)均只運行在一個線程中,但是不代表它只有一個線程。nodejs中許多異步方法在具體的實現時,內部均採用了多線程機制(具體後面會講到)。

 

事件輪詢

如果看過我前面博客的一些讀者可能知道,一個系統(或者說一個程序)中必須至少包含一個大的循環結構(我稱之爲“泵”),它是維持系統持續運行的前提。nodejs中一樣包含這樣的結構,我們叫它“事件輪詢”,它存在於主線程中,負責不停地調用開發者編寫的代碼。我們可以查看nodejs官方網站上對nodejs的說明:

我們可以看到,在nodejs中這個“循環”結構對開發者來講是不可見的。

那麼開發者編寫的代碼是怎樣通過事件輪詢來得到調用的呢?尤其是一些異步方法中帶的回調函數?看下面一張圖:

如上圖所示,每個異步函數執行結束後,都會在事件隊列中追加一個事件(同時保存一些必要參數)。事件輪詢下一次循環便可取出事件,然後會調用異步方法對應的回調函數(參數)。這樣一來,nodejs便能保證開發者編寫的每行代碼(每個回調)均在主線程中執行。注意這裏有一個問題,如果開發者在回調函數中調用了阻塞方法,那麼整個事件輪詢就會阻塞,事件隊列中的事件得不到及時處理。正因爲這樣,nodejs中的一些庫方法均是異步的,也提倡用戶調用異步方法。

其實看到這裏的時候,如果有對Windows編程(尤其對Windows界面編程)比較瞭解的讀者可能已經聯想到了Windows消息循環。

沒錯,nodejs中的事件輪詢原理跟Windows消息循環的原理類似。開發者編寫的代碼均運行在主線程中,如果你編寫了阻塞代碼,在Windows桌面程序中,由於消息得不到及時處理,界面就會卡死。

咱們再來看一下下面的nodejs代碼:

var fs = require('fs');
fs.readFile('hello.txt', function (err, data) {  //異步讀取文件
  console.log("read file end");
});
while(1)
{
    console.log("call readFile over");
}

如上,雖然我們使用異步方法讀取文件,但是文件讀取完畢後“read file end”永遠不會輸出,也就是說readFile方法的回調函數不會執行。原因很簡單,因爲後面的while循環一直沒退出,導致下一次事件輪詢不能開始,所以回調函數不能執行(包括其他所有回調)。事實再次證明,開發者編寫的所有代碼均只能運行在同一線程之中(姑且稱之爲主線程吧)。

 

關於異步方法

所謂異步方法,就是調用該方法不會阻塞調用線程,哪怕方法內部要進行耗時操作。你可以理解爲方法內部單獨開闢了一個新線程去處理任務,而調用異步方法僅僅是開啓這個新線程。下面的代碼模擬一個異步方法的內部結構(僅僅是模擬,不代表實際):

public void DoSomething(int arg1,AsyncCallback callback)
{
    (Action)(delegate()
    {
         Thread.Sleep(1000*20);  //模擬耗時操作
         if(callback != null)
         {
              callback(...);  //調用回調函數
         }
    }).BeginInvoke(null,null);

}

如上代碼所示,調用DoSomething方法不會阻塞調用線程。那麼對於每一個異步方法,怎樣去判斷異步操作是否執行完畢呢?這時候必須給異步方法傳遞一個回調函數作爲參數,在.NET中,這個回調參數一般是AsyncCallback類型的。如大家所熟知的FileStream.BeginRead/BeginWrite以及Socket.BeginReceive/BeginSend等等均屬於該類方法。

但是,我之所以要提異步方法,就是想讓大家區分nodejs中的異步方法和.NET中異步方法的一個重大區別,雖然兩者內部原理可以理解爲一致的,但是在回調函數的調用方式這一點上,兩者有截然不同的方式。

在.NET中,每個異步方法的回調函數均在另外一個線程中執行(非調用線程),而在nodejs中,每個異步方法的回調函數仍然還在調用線程上執行。至於爲什麼,大家可以看一下前面講事件輪詢的部分,nodejs中每個回調函數均由主線程中的事件輪詢來調用。這樣才能保證在nodejs中,開發者編寫的任何代碼均在同一個線程中運行(所謂的單線程)。

注:不懂調用線程、當前線程是什麼意思的同學可以看一下這篇博客:《高屋建瓴:梳理編程約定》


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章