天天看點

《Node學習指南》一1.3 異步函數及Node事件循環

本節書摘來自異步社群《node學習指南》一書中的第1章,第1.3節,作者【美】shelley powers,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

node學習指南

node的基本設計原則是将應用程式放置在單線程(或單程序)中執行,同時異步處理所有事件。

考慮下典型的web伺服器(如apache)是如何工作的。apache可以采用兩種不同的方式處理傳入的請求:一種方式是将傳入的每個請求配置設定到獨立的程序中直至請求被處理完畢;另一種方式則是為每一個請求生成單獨的處理線程。

第一種方式(也稱為prefork multiprocessing model,或prefork mpm)可以根據apache配置檔案中指定的值建立多個子程序。使用程序的優勢在于被請求的應用(如php應用)無需考慮線程安全問題;缺點是每個程序占用獨立記憶體,記憶體消耗大,應用的擴充性也不是很好。

第二種方式(也稱為worker mpm)是程序-線程混合方式。apache為傳入的每個請求建立一個新的處理線程,這樣對記憶體的使用更加有效,但這種方式要求應用必須是線程安全的。雖然現在流行的php語言是線程安全的,但卻無法保證和它一起被使用的各種庫也是線程安全的。

不管哪種方法,它們都可以應對并發請求。如果五個使用者在同一時間通路一個web應用,并且伺服器也進行了相應設定,那麼web伺服器就可以同時處理五個請求。

node的處理方式與上面兩種不同。當您啟動node應用程式時,它會被建立并運作在一個單線程上。node會等待應用程式啟動完成并開始捕獲請求。在未處理完目前請求時,其他請求是不能被處理的。

這種處理方式聽起來并不是很有效率,如果node是通過事件循環和回調函數實作異步運作(在node中,事件循環一般指輪詢指定事件類型并在合适的時間調用事件處理程式,而回調函數就是事件處理程式)的話,它是不應該低效的。

實際上,與一般單線程應用不同,當node應用程式接收到使用者請求時,雖然它會嚴格按照請求順序初始化這些資源請求操作(如資料庫請求或檔案通路),但并不會一直等待操作完成或結果傳回。相反,它會在操作請求中附加回調函數。當任何被請求的資源準備好或被請求的操作完成時,特定的事件會被觸發,關聯的回調函數也會被執行,回調函數會用請求到的資源或操作結果來做另一些事情。

如果五個使用者在同一時間通路node應用程式,并且該應用程式需要通路同一個檔案中的資源時,node會為每個檔案通路請求附加一個回調函數但并不等待傳回。當資源變為可用時,回調函數會被調用,最終依次滿足每個使用者的需求。在此期間,node應用仍然可以處理其他同樣或不同類型的使用者請求。

盡管node應用程式不是真正的并行處理使用者請求,但其設計方式使得應用能繁忙且高效地處理使用者請求,是以大多數人通常不會察覺到任何的響應延遲。最重要的是,它能非常有效地使用記憶體和其他有限的計算機資源。

為了描述node的異步特性,示例1-2修改了之前章節使用的hello world程式。它不再輸出“hello,world!”,而是打開先前建立的helloworld.js檔案并将其内容輸出給用戶端。

示例1-2 異步方式地打開檔案并寫入資料

《Node學習指南》一1.3 異步函數及Node事件循環

本示例中使用了一個新的檔案系統子產品(fs)。該子產品對标準的posix檔案操作進行了封裝,提供了包括打開檔案和通路檔案内容等操作。示例1-2使用了子產品中的readfile方法,并傳入了多個參數,包括檔案名稱、檔案編碼方式以及匿名回調函數。

在示例1-2中,我想指出兩個有關異步行為的執行個體,它們分别是附加在readfile方法和listen方法上的回調函數。

正如前面所讨論的,使用listen方法可以告訴http server對象監聽指定端口上的連接配接。node不會阻塞并等待連接配接建立,是以如果我們需要在連接配接建立時做些事情,就需要提供了一個回調函數,如示例1-2所示。

當網絡連接配接建立時會觸發監聽事件,該事件會觸發listen方法綁定的回調函數,進而将資訊輸出到控制台。

第二,也是更重要的執行個體是附加在readfile上的回調函數。相對來說,通路檔案是一個耗時的操作。如果一個單線程應用程式被多個客戶同時通路,而該應用處理每一個請求時都需要進行檔案通路操作的話,它可能很快就會陷入癱瘓而無法使用。

解決方法就是采用異步方式打開檔案和讀取檔案内容。隻有當内容已經讀入資料緩沖區(或讀取失敗時),附加在readfile方法上的回調函數才會被調用。錯誤資訊(如果有的話)和讀取到的資料(如果沒有錯誤發生時)會作為參數傳送給回調函數。

在回調函數中需要進行錯誤檢查,如果不存在錯誤,則将讀取到的資料傳回給用戶端。

大多數人使用javascript編寫用戶端應用程式,這些程式隻能被使用者在單個浏覽器中運作。而在服務端使用javascript編寫程式可能看上去會有些古怪和陌生。建立允許多人同時通路的javascript服務應用可能就更讓人覺得陌生了。

node的事件循環和異步函數調用可以幫助我們,這讓編寫服務端javascript程式變得容易且更有信心。但一定要注意的是,我們正在一個新的不同以往的環境中做javascript開發。

為了更好地描述新環境的不同,我建立了兩個新的應用:一個提供服務,另一個用于測試服務。示例1-3顯示了服務程式的代碼。

在代碼中,一個函數被調用,以同步方式按順序輸出從1~100的數字。然後程式以類似于示例1-2的方式打開一個檔案,但這次檔案名是以字元串參數的形式傳遞給函數的。此外,程式還使用了一個定時器,檔案打開操作被安排在定時器逾時之後執行。

示例1-3 輸出數字序列和檔案内容的服務程式

《Node學習指南》一1.3 異步函數及Node事件循環

輸出數字的循環體起到了延遲應用程式執行的效果,以便模拟密集計算過程,該過程會引起應用程式阻塞直到計算完成。在這裡settimeout是另一個異步函數,它會緊接着調用第二個異步函數:readfile。是以該應用程式結合了異步和同步流程。

建立一個名為main.txt的文本檔案,可以包含任何你想要的内容。運作應用程式并通過chrome浏覽器通路,通路時使用的url需要帶有file=name的查詢字段,應用程式将生成如下控制台輸出:

前兩行輸出資訊很容易了解。第一行由程式末尾console.log輸出,第二行是在檔案被打開時輸出的。但是,第三行的undefined.txt是怎麼回事?

其實,當處理來自浏覽器的web請求時,浏覽器可能會發送多個請求。例如,一般浏覽器可以發送第二個請求,尋找一個叫favicon.ico的檔案。正因為如此,當你在處理查詢字元串時,你必須檢檢視看需要的資料是否被提供,并忽略沒有資料的請求。

警告:

當期望從查詢字元串中擷取某些參數時,浏覽器發送多個請求的特點可能會影響到你的應用程式。是以,必須相應的調整應用,并在幾個不同的浏覽器上進行測試。

到目前為止,我們對node應用程式所做的所有測試都是從浏覽器中進行的。這樣我們無法對其進行壓力測試來展現node應用程式的異步特性。

示例1-4是一段非常簡單的測試代碼。它使用http子產品多次向服務程式發送請求。這些請求并不是按異步方式發送的。然而,我們同時也可以使用浏覽器通路該服務。兩者相結合,就可以達到異步測試應用程式的目的。

提示:

14章将介紹如何建立異步測試應用程式。

示例1-4 測試小程式,調用node服務程式2000次

《Node學習指南》一1.3 異步函數及Node事件循環

建立第二個文本檔案,并命名為secondary.txt。内容與main.txt有顯著不同即可。

在确定node服務程式運作起來後,啟動測試程式:

在測試程式運作的同時,使用浏覽器手動通路服務程式。觀察服務程式在控制台的輸出資訊,你會看到來自浏覽器的手動請求和來自測試程式的自動請求都能被處理。并且,結果與我們所期望的一緻,請求到的頁面中包含了如下資訊:

1到100的數字;

文本檔案的内容,在本示例中是main.txt的内容。

現在,讓我們嘗試做一些改動。在示例1-3中,将循環體中計數用的局部變量counter改為全局變量,并重新啟動應用程式。然後運作測試程式,并在浏覽器中通路該頁面。

輸出結果顯然發生改變。傳回的頁面内容不再是從1開始到100的數字,而是傳回從類似2601和26301這樣的數字開始的,按順序排列的連續99個數字,隻是初始值不同。

原因必然是因為使用了全局變量counter。因為在浏覽器中手動通路頁面時,自動測試程式也在做同樣的事,他們都會更新counter。另外由于手動和自動測試程式的請求被按照順序一個個的處理,是以沒有争用共享資料的情況發生(在多線程環境中,并行通路共享資料同時保證線程安全是最主要的問題),如果你之前有期望輸出一緻的起始值,這裡的結果可能會讓你感到些許意外。

現在再次更改應用程式,但這次我們删除變量app之前的var關鍵字(“不小心的”使其成為一個全局變量)。曾幾何時,在編寫用戶端javascript時,我們總是忘記使用var關鍵字。或許也隻有當我們程式中用到的某些庫使用了相同的變量名時,才會發現這種錯誤。

運作測試程式同時通過浏覽器手動通路node服務程式多次。你會發現浏覽器得到的頁面中偶爾會包含secondary.txt檔案的内容,而不是期望的main.txt檔案内容。這是因為在應用程式處理請求(帶有檔案名)和真正執行檔案打開操作之間有一段時間間隔,在此間隔期間測試程式的持續通路會使得服務程式修改app變量。測試程式之是以能夠引起這樣的問題,是因為我們做了一個異步功能調用,在異步調用開始執行而沒有完成前,node會放棄對目前請求處理過程的控制權來處理另一個使用者請求。

這個示例說明了正确使用var關鍵字在node中是至關重要的。