Node的異步I/O
我們?yōu)槭裁葱枰惒絀/O蟀俊?
- 用戶體驗
服務器端如果基于同步執(zhí)行的,隨著應用復雜性的增加,響應的總耗時為M+N+...的總時間诫尽,但是異步執(zhí)行的話,總耗時則為M炬守、N牧嫉、...中耗時最長的一個,能夠更快速響應資源减途,讓前端的體驗更好 - 資源分配
Node利用單線程酣藻,遠離多線程、狀態(tài)同步等問題鳍置,利用異步I/O辽剧,讓單線程遠離阻塞,以更好地利用CPU
異步I/O與非阻塞I/O
操作系統(tǒng)內(nèi)核對于I/O只有兩種方式:阻塞與非阻塞
阻塞I/O: 調(diào)用之后一定要等到系統(tǒng)內(nèi)核層面完成所有操作后墓捻,調(diào)用才結(jié)束
阻塞I/O造成CPU等待I/O抖仅,浪費等待時間,CPU的處理能力不能得到充分利用砖第。
非阻塞I/O:調(diào)用后會立刻不帶數(shù)據(jù)立刻返回(返回的僅僅是當前調(diào)用的狀態(tài))撤卢,要獲取數(shù)據(jù),還需要通過文件描述符進行再次讀取梧兼。
應用程序需要重復調(diào)用I/O操作來確認是否完成放吩,稱為輪詢
主要的輪詢技術(shù):
- read。最原始羽杰,性能最低的一種渡紫,通過重復調(diào)用來檢查I/O的狀態(tài)來完成完整數(shù)據(jù)的讀取
- select。通過對文件描述符的事件狀態(tài)來進行判斷(僅輪詢一次即可考赛,可以同時檢查1024個文件描述符惕澎,但是會持續(xù)等待到數(shù)據(jù)讀取完成為止)
- poll。與select類似颜骤,但是采用鏈表的方式來存儲狀態(tài)唧喉。其次它能避免不需要的檢查
- epoll。該方案是Linux下效率最高的的I/O事件通知機制忍抽,在進入輪詢的時候如果沒有檢查到I/O事件八孝,將會進行休眠,直到事件發(fā)生將它喚醒鸠项。不會浪費CPU干跛,執(zhí)行效率較高。
- kquue祟绊。與epoll類似楼入,不過僅在FreeBSD系統(tǒng)下存在
現(xiàn)實的異步I/O
通過讓部分線程進行阻塞I/O或者非阻塞I/O加輪詢技術(shù)來完成數(shù)據(jù)獲取哥捕,讓一個線程進行計算處理,通過線程之間的通信將I/O得到的數(shù)據(jù)進行傳遞浅辙,實現(xiàn)異步I/O
因此扭弧,Javascript只需在單線程(主線程)中執(zhí)行,內(nèi)部完成I/O任務的另有線程池记舆。
Node的異步I/O
事件循環(huán)
在進程啟動時鸽捻,Node便會創(chuàng)建一個類似while(true)的循環(huán),每執(zhí)行一次循環(huán)體的過程稱為Tick泽腮,每個Tick的過程就是查看是否有事件待處理御蒲,如果有,就取出事件及其相關(guān)的回調(diào)函數(shù)進而執(zhí)行诊赊,然后進入下個循環(huán)厚满,如果不再有事件處理,則跳出流程
觀察者
每個Tick過程中碧磅,由一個或多個觀察者來判斷是否有要處理的事件碘箍。
異步I/O、網(wǎng)絡請求等是事件的生產(chǎn)者鲸郊,源源不斷為Node提供不同類型的事件丰榴,這些事件被傳遞到對應的觀察者那里,事件循環(huán)則從觀察者那里取出事件并處里
請求對象
事實上秆撮,從Javascript發(fā)起調(diào)用到內(nèi)核執(zhí)行完成I/O操作的過渡過程中四濒,存在一種中間產(chǎn)物,叫做請求對象
拿fs.open()作為例子
(1)fs.open()根據(jù)路徑和參數(shù)去打開一個.cc文件(C++內(nèi)建模塊)职辨,從而得到一個文件描述符
(2)然后這個.cc文件經(jīng)過libuv平臺判斷調(diào)用對應平臺的uv_fs_open()方法
(3)在uv_fs_open()調(diào)用過程中盗蟆,創(chuàng)建了一個FSReqWrap請求對象,而從Javascript層傳入的參數(shù)和當前方法都會封裝到這個請求對象上舒裤,而回調(diào)函數(shù)則被設置在這個對象的oncomplete_sym屬性上
req_wrap->object->Set(oncpmplete_sym,callback);
(4)對象包裝完成后喳资,在Windows下,調(diào)用QueueUserWorkItem()方法將這個FSReaWrap對象推入線程池中等待執(zhí)行
QueueUserWorkItem(&uv_fs_thread_proc,req,WT_EXECUTEDEFAULT)
/*
接收三個參數(shù):
①將要執(zhí)行的方法的引用
②將要執(zhí)行的方法運行時所需要的參數(shù)
③執(zhí)行的標志
*/
(5)當有可用線程時腾供,會調(diào)用uv_fs_thread_proc()方法骨饿,這個方法會根據(jù)傳入?yún)?shù)的類型調(diào)用相應的底層函數(shù)。以uv_fs_open()為例台腥,實際上調(diào)用的是fs_open()方法
(6)至此,Javascript調(diào)用立即返回绒北,由Javascript層面發(fā)起的異步調(diào)用的第一階段就此結(jié)束黎侈,因此javascript可繼續(xù)執(zhí)行其他操作,從而達到異步的目的
執(zhí)行回調(diào)(處理請求對象)
(1)線程中的I/O操作調(diào)用完畢之后闷游,會將獲取的結(jié)果儲存在req->result屬性上峻汉,然后調(diào)用PostQueuedCompletionStatus()通知IOCP贴汪,告知當前對象操作已經(jīng)完成
PostQueuedCompletionStatus()方法的作用是向IOCP提交執(zhí)行狀態(tài),并把線程歸還線程池休吠,這個狀態(tài)可以通過GetQueuedCompletionStatus()提取
(2)每次Tick的執(zhí)行中扳埂,觀察者會調(diào)用IOCP相關(guān)的GetQueuedCompletionStatus()檢查線程池中是否有執(zhí)行完的請求,如果存在瘤礁,會把請求對戲那個加入到I/O觀察者的隊列中阳懂,然后將其當做事件處理
(3)取出請求對象的result屬性作為參數(shù),取出oncomplete_sym屬性(傳入的回調(diào)函數(shù))作為方法柜思,然后調(diào)用執(zhí)行岩调,以此達到調(diào)用Javascript中傳入的回調(diào)函數(shù)的目的。
總結(jié)
一個異步I/O經(jīng)歷了請求對象赡盘、I/O線程池号枕、觀察者、事件循環(huán)這四個步驟陨享,構(gòu)成了異步I/O模型的基本要素葱淳。windows下主要通過IOCP來向系統(tǒng)內(nèi)核發(fā)送I/O調(diào)用和從內(nèi)核獲取已完成的I/O操作,配以事件循環(huán)抛姑,以此完成異步I/O的過程赞厕。
非I/O的異步API
-
定時器
調(diào)用setTimeout()或者setInterval()創(chuàng)建的定時器會被插入到定時器觀察者內(nèi)部的一個紅黑樹中,每次Tick執(zhí)行時途戒,會從該紅黑樹中迭代取出定時器對象坑傅,檢查是否超過定時事件,如果超過就形成一個事件喷斋,它的回調(diào)函數(shù)將會被推入handles中排隊等候被執(zhí)行 -
process.nextTick()
每次調(diào)用process.nextTick()方法唁毒,只會將回調(diào)函數(shù)放入隊列中,在下一輪Tick取出執(zhí)行星爪。復雜度更低浆西,性能比setTimeout更高效。 -
setImmediate()
與process.nextTick()類似顽腾,但process.nextTick中的回調(diào)函數(shù)執(zhí)行的優(yōu)先級要高于setImmediate()近零,因為事件循環(huán)對觀察者的檢查是有先后順序的,process.nextTick()屬于idle觀察者抄肖,setImmeditate屬于check觀察者久信。
以上參考《深入淺出Node.js》一書