今天.NET老師在課堂上吹捧多線程編程,我就想為單線程抱個不平,因為Node的單線程非同步非阻塞I/O模型,演繹了單線程編程的神話。
阻塞I/O
程式執行過程中必然要進行很多I/O操作,讀寫檔案、輸入輸出、請求響應等等。I/O操作時最費時的,至少相對於代碼來說,在傳統的編程模式中,舉個例子,你要讀一個檔案,整個線程都暫停下來,等待檔案讀完後繼續執行。換言之,I/O操作阻塞了代碼的執行,極大地降低了程式的效率。
下面是是一個C#讀檔案的例子:
private string ReadTxtToStr(string filename)
{
//開啟檔案,開啟期間其他代碼停止執行,直到完成開啟後繼續執行代碼。
FileStream fs = File.Open(filename, FileMode.Open);
Console.WriteLine("我被開啟檔案阻塞了。");
StreamReader sr = new StreamReader(fs);
//讀取檔案,讀取期間其他代碼停止執行,直到完成讀取後繼續執行代碼。
string str=sr.ReadToEnd();
Console.WriteLine("我被讀取檔案阻塞了。");
return str;
}
在上述代碼中,兩個Console.WriteLine()雖然會被執行,但是卻被無辜地阻塞一段時間。理論上,如果讀取這個檔案需要10秒,我們就浪費了10秒在I/O等待中(實際程式運行中有很大一部分時間是浪費在I/O等待上的),在碼農眼裡這可是天文數字。
非阻塞I/O
理解了阻塞I/O,非阻塞I/O就好理解。非阻塞I/O是程式執行過程中,I/O操作不會阻塞程式的執行,也就是在I/O操作的同時,繼續執行其他代碼(這得益於Node的事件迴圈機制)。在I/O裝置效率還遠遠低於CPU效率的時代,這種I/O模型(非阻塞I/O)為程式帶來的效能上的提高是非常可觀的。
好,下面感受一下怎麼用Node.js實現非阻塞I/O,繼續讀檔案,看碼:
var fs = require("fs");
fs.readFile("./testfile", "utf8", function(error, file) {
if (error) throw error;
console.log("我讀完檔案了!");
});
console.log("我不會被阻塞!");
複製上面代碼儲存為test.js,並在同一目錄下建立一個名為testfile的檔案,用node命令運行test.js,你將看到以下輸出:
我不會被阻塞!
我讀完檔案了!
這顯然不符合傳統的程式執行順序,注意,這就是Node.js的非阻塞I/O了。
首先解釋下面程式,如果你熟悉JavaScript,請忽略。
var fs = require("fs");
以上代碼:引入Node.js內建的File System檔案系統模組fs。require()相當與Java的import,C++的include。
fs.readFile("./testfile", "utf8", function(error, file) {
if (error) throw error;
console.log("我讀完檔案了!");
});
以上代碼:進行I/O操作,給readFile綁定一個回呼函數function(error,file){},並在讀取testfile完成後執行回呼函數。期間,後面的代碼繼續執行,不受I/O阻塞。
這就是為什麼先看到“我不會被阻塞!”而後看到“我讀完檔案了!”的緣故。
Having asynchronous I/O is good, because I/O is more expensive than most code and we should be doing something better than just waiting for I/O.
Node.js事件輪詢機制(event loop)
《Node入門》推薦我們去讀一下Mixu的一篇關於事件輪詢的博文,的確值得一讀,我英語一般,開著詞典還能勉強看,略懂吧。
Mixu說的最經典的一句話:
Everything runs in parallel except your code!
(在Node中)除了代碼,一切都是並行的!
理解這句話,再去學Node,也就事半功倍了!