很久以前翻譯的,忘了出處(:з」∠)
首先需要知道的是,node.js的 I/O是異常昂貴的
The cost of I/O
L1-chache 3 cycles
L2-cache 14 cycles
RAM 250 cycles
Disk 41000000 cycles
Network 240000000 cycles
所以一旦當前的編程技術是通過等待I/O完成的話,那將是非常浪費的一件事情,現(xiàn)在有幾種方法可以處理對于性能的影響(可以參看異步套接字編程)
- 同步: 依次處理每個請求,并且每次只處理一個請求. 優(yōu)點: 非常的簡單,缺點: 一個請求就足以阻塞出整個應用
- 創(chuàng)建一個新的進程(fork a new process): 創(chuàng)建一個新的進程來處理新的請求. 優(yōu)點: 也很簡單,缺點: 多少個連接就意味著多少個進程,fork()是Unix程序員的錘子,因為所有問題看上就像個釘子,不過它通常都是多余的力量(overkill)
- 線程: 啟動一個新線程來處理請求. 優(yōu)點: 也很簡單,比起讓內(nèi)核(kernel)使用fork()來說要更加溫柔些,畢竟線程通常就有很多,而且資源開銷更小,缺點: 機器有可能并沒有好的線程程序,導致可能非常的復雜或降低速度,附送的還有對于共享資源的訪問也可能出現(xiàn)問題
其次,第二個問題是,線程共享的鏈接緩沖池,也是非常昂貴的
Apache 是多線程的,
每個請求都會生成一個線程或進程,它取決于conf,你可以看到這玩意是怎么吃內(nèi)存的,而且隨著并發(fā)連接數(shù)量的增多,所需要的線程也就越多,比如什么Nginx,節(jié)點等
而js并不是多線程,線程和進程會帶來沉重的內(nèi)存成本,所以它是基于事件的單線程,通過這樣來消除成千上萬的線程/進程,并且將所有需要處理的問題都綁定在一個線程上
Node.js 始終保持著單線程的優(yōu)良傳統(tǒng)
而它毫無疑問也是一個真正的單線程 : 在這里,你無法執(zhí)行任何的并行代碼,比如做一個延遲(sleep)來阻止服務器一秒:
while(new Date().getTime() < now + 1000) {
// do nothing
}
而在這期間,js不會回應任何來自客戶的請求,因為它只有一個線程執(zhí)行的代碼,或者如果你會一些別的什么cpu什么的代碼,說: 給我搞搞圖片大小,就算這樣,js依然會阻止這些請求
不過,不管什么都好,都是離不開并行的,尤其是你的代碼
代碼是沒辦法并行的運行在單個請求上的,不過,所幸所有的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();
}
);
如果你在一個請求中執(zhí)行了該操作,那么在這個數(shù)據(jù)庫處于阻塞(sleep)狀態(tài)的時候,你依然可以處理其他請求
這種方法可以很好的解決當你需要從同步走向異步/并發(fā)執(zhí)行時的問題
具有同步執(zhí)行時好的,因為它簡化了代碼(相對于并發(fā)問題,它可能會傾向于WTFS線程問題)
(Having synchronous execution is good, because it simplifies writing code (compared to threads, where concurrency issues have a tendency to result in WTFs).)
在node.js中,當你正在處理I/O的時候,不應該擔心后臺的問題,解決好你的回調(diào)問題,還有保證你的代碼不會被打斷,因為I/O并不會阻止其他請求,所以也不必承擔線程/進程的請求成本(比方說在Apache中的內(nèi)存開銷)
異步I/O是非常不錯的,因為I/O比大多數(shù)的代碼開銷更大,更昂貴,所以我們應該在等待I/O(阻塞過程)的過程中做更多的事情
(這個機制很大程度上是源于js本身就是一個非阻塞I/O)
事件輪詢(eventloop)是"一個解決和處理外部事件時將它們轉(zhuǎn)換為回調(diào)函數(shù)的調(diào)用的實體(entity)",所以當代碼調(diào)用一個I/O的時候,node.js可以從這個請求切換到另一個請求,當調(diào)用I/O的時候,代碼將會保存回調(diào)并且返回某些結(jié)果給node.js運行環(huán)境中,只有當數(shù)據(jù)實際可用的時候(或者說不那么阻塞了(sleep事件過了等)),回調(diào)便會被調(diào)用
當然,在后臺中,會有來自DB的線程和進程及其他進程在訪問,然而,這些都沒有明確的暴露給你的代碼,所以你大可以不必擔心來自其他I/O的干擾或相互作用,比方說,并不需要在意像數(shù)據(jù)庫或者別的異步進程這些,因為從請求的角度看來,這些線程的結(jié)構(gòu)都是通過事件輪詢返回到你的代碼中的,相比起Apache模型,少了許多線程和連接的開銷,因為線程不需要遍歷每個連接,只有當你必須要使用其他的并行操作等,哪怕這服務器管理是node.js,也阻止不了你
除了I/O調(diào)用外,node.js希望所有的請求都能迅速返回,比如說像當cpu快速密集處理一堆請求后,應盡快分離掉這些進程,你可以與事件或是通過使用一個抽象的玩意比如WebWorkers互動,這很顯然意味著你無法將你的代碼通過事件脫離一個后臺的線程獨自并行.基本上所有的對象發(fā)出事件(比如EventEmitter的實例)都是支持異步事件的互動的,你可以使用這種方式,比如使用文件阻塞代碼交互,sockets或是子進程等,這些都是可以在node.js中與事件進行互動的,多核的機子可以使用這些方法,(可以查閱關于Http與Node)
內(nèi)部實現(xiàn)
在內(nèi)部,node.js依賴于測試程序提供的時間循環(huán),并輔以libeio采用混合線程來提供異步I/O,想知道更多的話,可以查閱下libev documentation
js內(nèi)部同樣有事件輪詢機制
那么我們要怎樣在node.js中使用異步?
Tim Caswell描述了這么個模式在這( 網(wǎng)址失效了)
- 第一類函數(shù)(First-class-functions).我們將函數(shù)作為參數(shù)傳出去,在需要的時候執(zhí)行他們就可以了
- 復合函數(shù)(Function composition).也就是所謂的匿名函數(shù)或者是當I/O這些事件執(zhí)行后回調(diào)的函數(shù)
請求會將所有的事件都放入到隊列中,而node.js會不斷詢問是否有事件,如果存在事件那么便會立即執(zhí)行,如果執(zhí)行過程中存在阻塞事件的話,那么node.js會將它放到一個專門的線程池中專門執(zhí)行這些操作