一篇講解node.js事件迴圈的文章。原文出處:點擊跳轉!
在瞭解node.js之前你首先需要瞭解的一個基本的論點是:I/O是“昂貴”的。
因此對於當前的編程技術而言,最大的浪費來自於等待I/O的完成。下面列出了改善該問題的幾種方式,其中的某個可以協助你提高效能:
- 同步:在某一時刻,一次只處理一個請求。但這種情況下,任何一個請求都會“耽誤”(阻塞)所有其他的請求。
- fork一個新進程:對於每個請求,你啟動一個新的進程來處理。這種情況下,無法達到很好的擴充,上百個串連就意味著上百個進程的存在。fork()函數是Unix程式員的鎚子,因為使用它很方便,所以每個程式都看起來像個釘子一樣(都喜歡用鎚子拿來敲敲它)。所以,經常造成過度使用,而有些過往矯正。
- 線程:開啟一個新的線程來處理每個請求。這種方式很簡單,並且對於核心來講使用線程也比fork進程來得“親切”,因為通常線程花費比進程更少的開銷。缺點:你的機子可能不支援基於線程編程,並且基於線程的程式,其複雜度增長得非常快,同時你還會有對訪問共用資源的擔憂。
你需要瞭解的第二個論點是:被線程處理的每個串連都是“記憶體昂貴的”。
Apache是採用多執行緒請求的。它對於每個請求“孵化”出一個線程(或者進程,這取決於配置)來處理。你將會看到隨著並發串連數的增長以及更多的線程需要服務多個用戶端時,那些開銷有多消耗記憶體。Nginx跟Node.js都不是基於多執行緒模式的,因為線程跟進程都需要非常大的記憶體開銷。他們都是單線程的,但是基於事件的。這種基於單線程的模型消除了為了處理很多請求而建立成百上千個線程或進程帶來的開銷。
Node.js為你的代碼保持單線程的運行環境
它確實是基於單線程啟動並執行,你無法編寫任何代碼來執行並發;例如執行一個"sleep"操作將阻塞整個伺服器1秒鐘。
while(new Date().getTime() < now + 1000) { // do nothing}
因此,當代碼啟動並執行時候,node.js將不會響應來自用戶端的其他請求,因為它只有一個線程來執行你的代碼。或者,如果你有某些CPU密集型的操作,比如說,重設圖片的尺寸,那也將阻塞所有其他的請求。
...然而,除了你的代碼之外,其他的一切都是
並發執行的
在一個單獨的請求裡,沒有辦法可以使得代碼並存執行。然而,所有的I/O都是基於時間的並且是非同步,所以接下來的代碼將不會阻塞伺服器:
c.query( 'SELECT SLEEP(20);', function (err, results, fields) { if (err) { throw err; } res.writeHead(200, {'Content-Type': 'text/html'}); res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>'); c.end(); });
如果你在一個請求中這麼做,其他請求能夠很好得被執行。
為什麼這是更好的方式?什麼時候我們需要從同步轉向非同步/並發執行?
採用同步執行是個不錯的方式,因為它使得編碼變得容易(對比線程而言,並發問題常常讓你陷入萬劫不複)。
在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/
另外,轉載本文請著名“原文出處”,謝謝!