理解Node.js的事件循環(代碼是異步單線程,內部實現用的還是進程和線程,基於池化的線程實現異步)

在瞭解node.js之前你首先需要了解的一個基本的論點是:I/O是“昂貴”的。


因此對於當前的編程技術而言,最大的浪費來自於等待I/O的完成。下面列出了改善該問題的幾種方式,其中的某個可以幫助你提高性能:

  • 同步:在某一時刻,一次只處理一個請求。但這種情況下,任何一個請求都會“耽誤”(阻塞)所有其他的請求。
  • fork一個新進程:對於每個請求,你啓動一個新的進程來處理。這種情況下,無法達到很好的擴展,上百個連接就意味着上百個進程的存在。fork()函數是Unix程序員的錘子,因爲使用它很方便,所以每個程序都看起來像個釘子一樣(都喜歡用錘子拿來敲敲它)。所以,經常造成過度使用,而有些過往矯正。
  • 線程:開啓一個新的線程來處理每個請求。這種方式很簡單,並且對於內核來講使用線程也比fork進程來得“親切”,因爲通常線程花費比進程更少的開銷。缺點:你的機子可能不支持基於線程編程,並且基於線程的程序,其複雜度增長得非常快,同時你還會有對訪問共享資源的擔憂。

你需要了解的第二個論點是:被線程處理的每個連接都是“內存昂貴的”。

Apache是採用多線程處理請求的。它對於每個請求“孵化”出一個線程(或者進程,這取決於配置)來處理。你將會看到隨着併發連接數的增長以及更多的線程需要服務多個客戶端時,那些開銷有多消耗內存。Nginx跟Node.js都不是基於多線程模型的,因爲線程跟進程都需要非常大的內存開銷。他們都是單線程的,但是基於事件的。這種基於單線程的模型消除了爲了處理很多請求而創建成百上千個線程或進程帶來的開銷。


Node.js爲你的代碼保持單線程的運行環境


它確實是基於單線程運行的,你無法編寫任何代碼來執行併發;例如執行一個"sleep"操作將阻塞整個服務器1秒鐘。

[javascript] view plaincopyprint?
  1. while(new Date().getTime() < now + 1000) {  
  2.    // do nothing  
  3. }  

因此,當代碼運行的時候,node.js將不會響應來自客戶端的其他請求,因爲它只有一個線程來執行你的代碼。或者,如果你有某些CPU密集型的操作,比如說,重置圖片的尺寸,那也將阻塞所有其他的請求。


...然而,除了你的代碼之外,其他的一切都是併發執行


在一個單獨的請求裏,沒有辦法可以使得代碼並行執行。然而,所有的I/O都是基於時間的並且是異步的,所以接下來的代碼將不會阻塞服務器:

[javascript] view plaincopyprint?
  1. c.query(  
  2.    'SELECT SLEEP(20);',  
  3.    function (err, results, fields) {  
  4.      if (err) {  
  5.        throw err;  
  6.      }  
  7.      res.writeHead(200, {'Content-Type''text/html'});  
  8.      res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');  
  9.      c.end();  
  10.     }  
  11. );  

如果你在一個請求中這麼做,其他請求能夠很好得被執行。


爲什麼這是更好的方式?什麼時候我們需要從同步轉向異步/併發執行?


採用同步執行是個不錯的方式,因爲它使得編碼變得容易(對比線程而言,併發問題常常讓你陷入萬劫不復)。

在node.js中,你不需要去擔心你的代碼在後端會發生。你只需要在你做I/O操作的時候使用回調就可以了。你會得到保證:你的代碼不會被中斷,並且I/O操作也不會阻塞其他請求(因爲沒有了那些線程/進程需要花費的開銷,比如在Apache中會發生的內存過高等)。

採用異步I/O也很好,因爲I/O比那些執行其他操作更昂貴,我們應該做一些更有意義的事情而不是去等待I/O。


一個事件循環指的是——一個實體,它可以處理外部事件並且將它們轉化爲回調的執行。因此,I/O調用變成了node.js可以從一個請求切換到另外一個請求的“點”,你的代碼保存了回調並返回控制權給node.js運行時環境。而回調在最終獲得了數據之後被執行。

當然,在node.js內部,仍然是依靠線程和進程來進行數據訪問、處理其他任務執行。然而,這些都沒有明確地對你的代碼暴露出來,所以你不需要額外擔心內部如何處理I/O之間的交互。對比Apache的模型,它少去了很多線程以及線程開銷,因爲對每個連接來講單獨的線程不是必須的。僅僅是當你絕對需要讓某個操作併發執行纔會需要線程,但即便如此線程也是node.js自己管理的。

除了I/O調用之外,node.js期待所有的請求最好快速返回。比如,那些CPU密集型的工作應該被隔離到另一個進程上去執行(通過與事件交互或者使用像WebWorker一樣的抽象)。這很明顯意味着當你與事件交互的時候,如果沒有另一個線程在後端(node.js運行時),那麼你是無法並行化執行代碼的。基本上,所有可以emit事件的對象(例如EventEmitter的實例)都支持基於事件的異步交互並且你也可以與“blocking code”交互(例如使用文件、sockets或者在node.js中是EventEmitter的子進程)。使用這種方案的話,就能夠很好得利用多核的優勢了,可以看看:node-http-proxy。

內部實現

內部,node.js依賴於libev提供的事件循環,libeio是對於libev的補充,node.js使用池化的線程來提供對於異步I/O的支持。如果你想了解更多細節,你可以看一下libev的文檔


如何在Node.js中實現異步


Tim Caswell在其PPT中描述了整個模式:

  • First-classfunction:例如我們將function作爲數據傳遞,包裹他們以在需要的時候執行。
  • Function組裝:就像你瞭解的關於異步函數或者閉包一樣,在觸發了I/O事件之後執行。
  • 回調計數器:對於基於事件的回調,你無法保證對於任何特殊的命令,I/O事件都會被執行。所以,一旦你需要多次查詢來完成某個處理,通常你僅需要對任何的併發I/O操作進行計數,然後在你確實需要最後的結果的時候檢查是否必要的操作都已全部完成(其中的一個例子是在事件回調中,通過對返回的數據庫查詢進行計數)。查詢會被併發執行,並且I/O也對此提供支持(例如可以通過連接池的方式實現併發查詢)。
  • 事件循環:上面已經提到過,你可以將blockingcode包裹進一個基於事件的抽象中去(比如通過運行一個子進程,然後當它執行完成之後再返回)。

真的非常簡單!

再次申明原文出處:http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/

另外,轉載本文請著名“原文出處”,謝謝!

發佈了4 篇原創文章 · 獲贊 6 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章