單線程編程會因阻塞I/O導致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態(tài)同步等問題;利用異步I/O,讓單線程遠離阻塞,以好使用CPU。
異步IO的實現(xiàn)
在Node中,JS是在單線程中執(zhí)行的沒錯,但是內(nèi)部完成IO工作的另有線程池,使用一個主進程和多個IO線程來模擬異步IO。
當主線程發(fā)起IO調(diào)用時,IO操作會被放在IO線程來執(zhí)行,主線程繼續(xù)執(zhí)行下面的任務,在IO線程完成操作后會帶著數(shù)據(jù)通知主線程發(fā)起回調(diào)。
事件循環(huán)
這是Node的執(zhí)行模型,正是這種模型使得回調(diào)函數(shù)非常普遍。
在進程啟動時,Node便會創(chuàng)建一個類似while(true)的循環(huán),執(zhí)行每次循環(huán)的過程就是判斷有沒有待處理的事件,如果有,就取出事件及其相關(guān)的回調(diào)并執(zhí)行他們,然后進入下一個循環(huán)。如果不再有事件處理,就退出進程。
觀察者
在每個循環(huán)中,怎么判斷是否有事件需要處理呢?這里就要引入觀察者了。每個事件循環(huán)中都有一個或多個觀察者,而判斷是否有事件要處理的過程就是向這些觀察者詢問是否有要處理的事件。
事件循環(huán)是一個典型的生產(chǎn)者/消費者模型,異步IO,網(wǎng)絡請求等是事件的生產(chǎn)者,源源不斷的為Node提供不同類型的事件,這些事件被傳遞到觀察者那里,事件循環(huán)則從觀察者那里取出事件并處理。
請求對象
對于Node中的異步IO調(diào)用而言,回調(diào)函數(shù)不由開發(fā)者來調(diào)用,從JS發(fā)起調(diào)用到IO操作完成,存在一個中間產(chǎn)物,叫請求對象。
在JS發(fā)起調(diào)用后,JS調(diào)用Node的核心模塊,核心模塊調(diào)用C++內(nèi)建模塊,內(nèi)建模塊通過libuv判斷平臺并進行系統(tǒng)調(diào)用。在進行系統(tǒng)調(diào)用時,從JS層傳入的方法和參數(shù)都被封裝在一個請求對象中,請求對象被放在線程池中等待執(zhí)行。JS立即返回繼續(xù)下面的操作。
執(zhí)行回調(diào)
在線程可用時,線程會取出請求對象來執(zhí)行IO操作,執(zhí)行完后將結(jié)果放在請求對象中,并歸還線程。
在事件循環(huán)中,IO觀察者會不斷的找到線程池中已經(jīng)完成的請求對象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。
流程圖
非IO的異步API
定時器
setTimeout()和setInterval()
這兩個方法實現(xiàn)原理與異步IO相似,只不過不用線程池的參與。
使用它們創(chuàng)建的定時器會被放入定時器觀察者中,每次事件循環(huán)執(zhí)行時會從觀察者中取出并判斷是否超過定時時間,超過就形成一個事件,回調(diào)立即執(zhí)行。
所以,和瀏覽器中一樣,這個并不精確。
process.nextTick()
有時我們想要立即異步執(zhí)行一個任務,可能會使用延時為0的定時器,但是這樣開銷很大。我們可以換而使用這個,這個會將傳入的回調(diào)放入隊列中,下一輪Tick(事件循環(huán))中取出執(zhí)行。
process.nextTick(function(){console.log("nextTick");});
console.log("thisTick");
setImmediate()
這個函數(shù)表現(xiàn)上與process.nextTick()相同,但是還是有細微的區(qū)別
當setImmediate()遇上process.nextTick()時,process.nextTick()的優(yōu)先級高于setImmediate(),這里是因為事件循環(huán)對于觀察者的檢查是有順序的,process.nextTick()屬于idle觀察者,setImmediate()屬于check觀察者。
idle觀察者優(yōu)于IO觀察者優(yōu)于check觀察者。
而且,對于process.nextTick()的回調(diào)函數(shù),是保存在一個數(shù)組中的,當有多個時,會在下一個Tick全部執(zhí)行完,而setImmediate()的回調(diào)函數(shù)們在一個鏈表中,每輪Tick只執(zhí)行一個。這樣設計是為了防止一次循環(huán)持續(xù)過久,CPU過多占用。(這個特性新版本好像取消了,也是一次循環(huán)執(zhí)行完,可以用下面的例子測試下)。
process.nextTick(function () {
console.log('nextTick執(zhí)行1');
});
process.nextTick(function () {
console.log('nextTick執(zhí)行2');
});
setImmediate(function () {
console.log('setImmediate執(zhí)行1');
process.nextTick(function () {
console.log('勢入');
});
});
setImmediate(function () {
console.log('setImmediate執(zhí)行2');
});
console.log('正常執(zhí)行');
事件驅(qū)動與服務器
網(wǎng)絡IO事件也同樣的應用到了異步IO。網(wǎng)絡上的請求都會交給IO觀察者來處理。
經(jīng)典的服務器有下面幾種:
同步式
每進程每請求
每線程每請求
上面的模型都有在大量請求下性能下降的問題。
Node通過事件驅(qū)動來處理請求,每個請求不必創(chuàng)建新的線程,開銷小。