nodejs筆記-異步I/O

1.為什么要使用異步I/O

1.1 用戶體驗

瀏覽器中的Javascripts是在單線程上執(zhí)行的付翁,并且和UI渲染公用一個線程项秉。這就意味著在執(zhí)行Javascript時候UI的渲染和響應(yīng)是出于停滯的狀態(tài)樟凄,如果腳本執(zhí)行時間超過100ms用戶就能感受到頁面卡頓僵蛛。在B/S模型中如果通過同步方式獲取服務(wù)器資源Javascript需要等待資源的返回荞驴,這段時間UI將會停頓不響應(yīng)交互琐谤。而采用異步方式請求資源的同時Javascript和UI渲染可以繼續(xù)執(zhí)行。

通過異步執(zhí)行可以消除UI阻塞現(xiàn)象魄懂,但是獲取資源速度取決于服務(wù)器的響應(yīng)沿侈,假設(shè)有這么個場景,獲取兩個資源數(shù)據(jù):

get('json_a');//需要消耗時間M
get('json_b');//需要消耗時間N

如果采用同步方式獲取資源的時間為M+N市栗,如果采用異步方式時間則是max(M,N)缀拭。隨著網(wǎng)站的擴大,數(shù)據(jù)將會分布在不同服務(wù)器上填帽,分布式也將意味著M與N的值會線性增長蛛淋。同步與異步的耗時差距也會變大。

1.2 資源的分配

假設(shè)一組互不先關(guān)的任務(wù)需要執(zhí)行篡腌,主流方法有兩種:

  • 單線程串行依次執(zhí)行
  • 多線程并行完成

如果創(chuàng)建多線程的開銷小于并行執(zhí)行褐荷,那么多線程的方式是首選。多線程的代價在于創(chuàng)建線程和執(zhí)行線程時的上下文切換嘹悼。在復(fù)雜業(yè)務(wù)中多線程需要面臨鎖叛甫、狀態(tài)同步問題。優(yōu)勢在于多線程在多核CPU上可以提升CPU利用率绘迁。

單線程串行執(zhí)行缺點在于性能合溺,任意一個任務(wù)略慢都會影響下一個執(zhí)行。通常I/O與CPU計算之間是可以并行進行的缀台,但是同步編程導(dǎo)致I/O的進行會讓后續(xù)任務(wù)等待棠赛,造成資源浪費。

Node在兩者之間做出了自己的方案:利用單線程膛腐,遠離多線程死鎖睛约、狀態(tài)同步問題;利用異步I/O哲身,讓單線程遠離阻塞辩涝,更好的利用CPU。

為了彌補單線程無法利用多核CPU缺點勘天,Node提供了類似前端瀏覽器的Web Workers的子進程怔揩,子進程可以通過工作進程高效的利用CPU和I/O。

異步I/O調(diào)用示意圖

[異步I/O調(diào)用示意圖]

2.異步I/O實現(xiàn)

2.1異步I/O與非阻塞I/O

操作系統(tǒng)內(nèi)核對于I/O只有兩種方式:阻塞和非阻塞脯丝。調(diào)用阻塞I/O時商膊,程序需要等待I/O完成才返回結(jié)果,如圖:

調(diào)用阻塞I/O的過程

為了提高性能宠进,內(nèi)核提供了非阻塞I/O晕拆,非阻塞I/O調(diào)用之后會立刻返回,如圖:

調(diào)用非阻塞I/O的過程

非阻塞I/O返回后材蹬,完整的I/O并沒有完成实幕,立即返回的不是業(yè)務(wù)層期望的數(shù)據(jù)吝镣,僅僅是當前調(diào)用狀態(tài)。為了獲取完整的數(shù)據(jù)昆庇,應(yīng)用需要反復(fù)調(diào)用I/O操作來確認是否完成末贾。這種反復(fù)調(diào)用判斷操作是否完成的計算叫做 輪詢

現(xiàn)存的輪詢技術(shù)主要有這些:

  1. read
    最原始的一種方式凰锡,通過反復(fù)調(diào)用I/O狀態(tài)來完成數(shù)據(jù)讀取未舟,在獲取最終數(shù)據(jù)前圈暗,CPU一直耗用在等待是掂为,示意圖:
通過read進行輪詢的示意圖
  1. select
    在read基礎(chǔ)上的改進方案,通過文件描述符上的事件狀態(tài)來進行判斷员串,select輪詢有一個限制勇哗,它采用一個1024長度的數(shù)組來保存儲存狀態(tài),所以它最多可以檢查1024個文件描述符寸齐,示意圖:
通過select進行輪詢示意圖
  1. poll
    采用鏈表的方式來避免數(shù)組長度限制欲诺,能避免不需要的檢查。當文件描述符多時渺鹦,性能還是十分低下扰法,于select相似,性能有所改善毅厚,如圖:
通過poll進行輪詢示意圖
  1. epoll
    Linux下效率最高的I/O事件通知機制塞颁,進入輪詢時如果沒有檢查到I/O事件,將會進行休眠吸耿,直到事件將他喚醒祠锣。利用的事件通知、執(zhí)行回調(diào)方式咽安,而不是遍歷查詢伴网,所以不會浪費CPU,執(zhí)行效率比較高妆棒。示意圖:
通過epoll進行輪詢示意圖
  1. kqueue
    與epoll類似澡腾,只存在FreeBSD系統(tǒng)下。

2.2 理想的非阻塞異步I/O

期望的完美異步I/O應(yīng)該是程序發(fā)起非阻塞調(diào)用糕珊,無需通過遍歷或者事件喚醒等輪詢方式动分,可以進行下一個任務(wù),只需要在I/O完成后通過信號或回調(diào)將數(shù)據(jù)傳遞給應(yīng)用程序放接,示意圖:

理想異步I/O示意圖

2.3現(xiàn)實的異步I/O

通過讓部分線程進行阻塞I/O或者非阻塞I/O加輪詢技術(shù)完成數(shù)據(jù)獲取刺啦。讓一個線程進行處理計算,通過線程之間的通訊將I/O得到的數(shù)據(jù)進行傳遞纠脾,實現(xiàn)異步I/O,示意圖:

異步I/O

最初Node在*nix平臺下采用libeio配合libev實現(xiàn)I/O異步I/O,Node v0.9.3中玛瘸,自行實現(xiàn)了線程池完成異步I/O蜕青。
windows下通過IOCP來實現(xiàn)(實現(xiàn)原理仍然是線程池,只是由系統(tǒng)內(nèi)核接受管理)糊渊。

windows和*nix平臺的差異右核,Node提供了libuv作為封裝,兼容性判斷由這一層完成渺绒,Node編譯期間會判斷平臺條件贺喝。

3.Node的異步I/O

3.1事件循環(huán)

啟動Node時會創(chuàng)建一個類似while(true)的循環(huán),每執(zhí)行一次循環(huán)過程稱之為Tick宗兼。每個Tick的過程就是檢查是否有待處理事件躏鱼,如果有,就讀取出事件及其相關(guān)的回調(diào)函數(shù)殷绍,如果存在關(guān)聯(lián)的回調(diào)函數(shù)染苛,就執(zhí)行。然后加入下一個循環(huán)主到,如果不再有事件處理就退出進程茶行。如圖:

Tick流程圖

3.2觀察者

在每個Tick過程中,怎么判斷是否有事件需要處理呢登钥?畔师,這里引入了觀察者概念。
每個事件循環(huán)中有一個或多個觀察者牧牢,而判斷是否有事件要處理的過程就是向觀察者詢問是否需要處理事件看锉。

3.3請求對象

Javascript發(fā)起調(diào)用到內(nèi)核執(zhí)行完I/O操作的過程中,存在一種中間產(chǎn)物结执,叫做請求對象度陆。
以fs.open()為例:

fs.open = function(path, flags, mode, callback) {
    //...
    binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback);
}

fs.open()是根據(jù)指定路徑和參數(shù)打開一個文件,從而獲取一個文件描述符献幔,這是后續(xù)所有I/O操作的初始操作懂傀。
Javascript層面的代碼調(diào)用C++核心模塊進行下層操作。示意圖:

調(diào)用示意圖

實際上調(diào)用了uv_fs_open()方法蜡感。在調(diào)用過程中創(chuàng)建了一個FSReqWrap請求對象蹬蚁。從Javascriptc層傳入的參數(shù)和當前方法都封裝在這個請求對象中,回調(diào)函數(shù)則被設(shè)置在對象的oncomplete_sym屬性上:

req_wrap->object_->Set(oncomplete_sym, callback);

對象包裝完畢郑兴,將FSReqWrap對象推入線程池中等待執(zhí)行犀斋。此時Javascript調(diào)用立即返回,Javascript線程可繼續(xù)執(zhí)行當前任務(wù)的后續(xù)操作情连,當前的I/O操作在線程池中等待執(zhí)行叽粹,不管是否是阻塞I/O,的不會影響Javascript線程的后續(xù)執(zhí)行。
請求對象是異步I/O過程的重要中間產(chǎn)物虫几,所有狀態(tài)都保存在這個對象中锤灿,包括送入線程池執(zhí)行以及I/O操作完畢后的回調(diào)處理。

3.4執(zhí)行回調(diào)

線程池中的I/O操作調(diào)用完畢后辆脸,將獲取結(jié)果儲存在req->result屬性上但校,然后通知IOCP(windows下)告知操作已完成,并歸還線程到線程池啡氢。
在每次Tick的執(zhí)行中状囱,它會檢查線程池中是否有執(zhí)行完的請求,如果存在倘是,將請求對象加入I/O觀察者列隊中亭枷,然后將其當做事件處理。
I/O觀察者回調(diào)函數(shù)的行為就是取出請求對象的result屬性作為參數(shù)然后執(zhí)行回調(diào)辨绊,調(diào)用Javascript中傳入的回調(diào)函數(shù)奶栖,至此,這個I/O流程完全接受门坷,示意圖:

整個異步I/O流程

在Node中除了Javascript是單線程外,Node自身其他都是多線程的袍镀,除了用戶代碼無法并行執(zhí)行默蚌,所有I/O則是可以并行起來的。

4.非I/O的異步API

無關(guān)I/O的異步API

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

4.1定時器

setTimeout()與setInterval()與瀏覽器API一致苇羡,分別用于單次和多次定時執(zhí)行任務(wù)绸吸。調(diào)用它們時創(chuàng)建的定時器會被插入到定時器觀察者內(nèi)部的一個紅黑樹中,每次Tick執(zhí)行會到該紅黑樹中迭代取出定時器對象设江,檢測是否超時锦茁,如果超時則形成一個事件,它的回調(diào)函數(shù)立即執(zhí)行叉存。
定時器并非精確码俩,雖然循環(huán)非常快但是如果某一次計算占用循環(huán)事件特別多歼捏,那么下次循環(huán)稿存,它可能已經(jīng)超時很久了。
setTimeout()的行為圖:

setTimeout()的行為

4.2 process.nextTick()

如果需要一個立即異步執(zhí)行的任務(wù)瞳秽,可以這樣調(diào)用:

setTimeout(() =>{
    //todo
}, 0);

process.nextTick(() => {
    //todo
})

由于定時器需要調(diào)用紅黑樹所有比較浪費性能瓣履。process.nextTick()方法比較輕量。每次調(diào)用process.nextTick()方法练俐,只會把回調(diào)函數(shù)放入隊列中袖迎,在下一輪Tick時取出立即執(zhí)行。所有process.nextTick()更為高效。

4.3 setImmediate()

setImmediate()與process.nextTick()相似燕锥,都是將回調(diào)函數(shù)延遲執(zhí)行浴韭,process.nextTick()執(zhí)行回調(diào)優(yōu)先級高于setImmediate()。

process.nextTick(() => {
    console.log('process.nextTick');
})
setImmediate(() => {
    console.log('setImmediate');
})
console.log('正常執(zhí)行')
//執(zhí)行結(jié)果
正常執(zhí)行
process.nextTick
setImmediate

這是因為事件循環(huán)對觀察者的檢查是有先后順序的脯宿,process.nextTick()屬于idle觀察者念颈,setImmediate()屬于check觀察者。
process.nextTick()的回調(diào)函數(shù)保存在一個數(shù)組中连霉,setImmediate()的結(jié)果則是保存在鏈表中榴芳。process.nextTick()在每次循環(huán)中會將數(shù)組的回調(diào)函數(shù)全部執(zhí)行完畢,而setImmediate()每輪循環(huán)中執(zhí)行鏈表中的一個回調(diào)函數(shù) (當前運行node版本是windows8.7.0,新版的setImmediate處理回調(diào)函數(shù)已經(jīng)改變跺撼,在一輪循環(huán)中setImmediate中的回調(diào)函數(shù)被全部執(zhí)行)窟感。
列如:

process.nextTick(() => {
    console.log('nextTick執(zhí)行1');
})
process.nextTick(() => {
    console.log('nextTick執(zhí)行2');
})
setImmediate(() => {
    console.log('setImmediate執(zhí)行1');
    process.nextTick(() => {
        console.log('插入執(zhí)行');
    })
})
setImmediate(() => {
    console.log('setImmediate執(zhí)行2');
})
console.log('正常執(zhí)行')
//執(zhí)行結(jié)果
正常執(zhí)行
nextTick執(zhí)行1
nextTick執(zhí)行2
setImmediate執(zhí)行1
setImmediate執(zhí)行2
插入執(zhí)行

5.事件驅(qū)動與高性能服務(wù)器

利用Node構(gòu)建web服務(wù)器流程圖:

利用Node構(gòu)建web服務(wù)器流程圖

服務(wù)器模型的經(jīng)典模型:

  • 同步式
    一次只能處理一個請求,其余請求出于等待狀態(tài)
  • 每進程/每請求
    為每個請求創(chuàng)建一個進程歉井,可以同時處理多個請求柿祈,不具備高擴展性,系統(tǒng)資源有限哩至。
  • 每線程/每請求
    為每個請求啟動一個新線程躏嚎。比啟動新進程輕量,但是高并發(fā)的時候內(nèi)存將很快耗光菩貌。(Apache采用這種模式)卢佣,線程多了后上下文切換頻繁消耗資源。

Node采用事件驅(qū)動方式無需為每個請求創(chuàng)建新線程箭阶,可以省掉很多開銷(Nginx采用與Node相同的事件驅(qū)動)虚茶,即使在大并發(fā)的情況下也不受上下文切換開銷的影響。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仇参,一起剝皮案震驚了整個濱河市嘹叫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诈乒,老刑警劉巖罩扇,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異抓谴,居然都是意外死亡暮蹂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門癌压,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仰泻,“玉大人,你說我怎么就攤上這事滩届〖睿” “怎么了被啼?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長棠枉。 經(jīng)常有香客問我浓体,道長,這世上最難降的妖魔是什么辈讶? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任命浴,我火速辦了婚禮,結(jié)果婚禮上贱除,老公的妹妹穿的比我還像新娘生闲。我一直安慰自己,他們只是感情好月幌,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布碍讯。 她就那樣靜靜地躺著,像睡著了一般扯躺。 火紅的嫁衣襯著肌膚如雪捉兴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天录语,我揣著相機與錄音倍啥,去河邊找鬼。 笑死钦无,一個胖子當著我的面吹牛逗栽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播失暂,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鳄虱!你這毒婦竟也來了弟塞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拙已,失蹤者是張志新(化名)和其女友劉穎决记,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倍踪,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡系宫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了建车。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扩借。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缤至,靈堂內(nèi)的尸體忽然破棺而出潮罪,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布嫉到,位于F島的核電站沃暗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏何恶。R本人自食惡果不足惜孽锥,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望细层。 院中可真熱鬧惜辑,春花似錦、人聲如沸今艺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虚缎。三九已至撵彻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間实牡,已是汗流浹背陌僵。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留创坞,地道東北人碗短。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像题涨,于是被迫代替她去往敵國和親偎谁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容