nodejs事件輪詢詳述

目錄html

 

概述node

關於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中代碼是非阻塞的。網站

單線程ui

對這個概念有誤解的人很是之多,覺得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中,開發者編寫的任何代碼均在同一個線程中運行(所謂的單線程)。

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

相關文章
相關標籤/搜索