異步I/O

為什么要異步 I/O

用戶體驗(yàn)

只有后端能夠快速響應(yīng)資源或听,才能讓前端的體驗(yàn)變好

資源分配

利用單線程妥粟,遠(yuǎn)離多線程死鎖、狀態(tài)同步等問(wèn)題窝趣;利用異步 I/O疯暑,讓單線程遠(yuǎn)離阻塞,以更好地使用 CPU

異步 I/O 實(shí)現(xiàn)現(xiàn)狀

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

輪詢技術(shù)滿足了非阻塞 I/O 確保獲取完整數(shù)據(jù)的需求哑舒,但是對(duì)于應(yīng)用程序而言妇拯,它仍然只能算是一種同步,因?yàn)閼?yīng)用程序仍然需要等待 I/O 完全返回洗鸵,依舊花費(fèi)了很多時(shí)間等待越锈。等待期間,CPU 要么用于遍歷文件描述符的狀態(tài)预麸,要么用于休眠等待時(shí)間發(fā)生

理想的非阻塞異步 I/O

我們期望的完美的異步 I/O 應(yīng)該是應(yīng)用程序發(fā)起非阻塞調(diào)用瞪浸,無(wú)須通過(guò)遍歷或者事件喚醒等方式輪詢,可以直接處理下一個(gè)任務(wù)吏祸,只需在 I/O 完成后通過(guò)信號(hào)或回調(diào)將數(shù)據(jù)傳遞給應(yīng)用程序即可

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

通過(guò)讓部分現(xiàn)成進(jìn)行阻塞 I/O 或者非阻塞 I/O 加輪詢技術(shù)來(lái)完成數(shù)據(jù)獲取对蒲,讓一個(gè)線程進(jìn)行計(jì)算處理,通過(guò)線程之間的通信將 I/O 得到的數(shù)據(jù)進(jìn)行傳遞贡翘,這就輕松實(shí)現(xiàn)了異步 I/O(盡管它是模擬的)


基于libuv的架構(gòu)示意圖.png

我們時(shí)常提到 Node 是單線程的蹈矮,這里的單線程僅僅只是 JavaScript 執(zhí)行在單線程中罷了。在 Node 中鸣驱,無(wú)論是 *nix 還是 Windows 平臺(tái)泛鸟,內(nèi)部完成 I/O 任務(wù)的另有線程池

Node 的異步 I/O

事件循環(huán)——Node 自身的執(zhí)行模型

在進(jìn)程啟動(dòng)時(shí),Node 便會(huì)創(chuàng)建一個(gè)類似于 while(true) 的循環(huán)踊东,每執(zhí)行一次循環(huán)體的過(guò)程我們成為 Tick北滥。每個(gè) Tick 的過(guò)程就是查看是否有事件待處理,如果有闸翅,就取出事件及其相關(guān)的回調(diào)函數(shù)再芋。如果存在關(guān)聯(lián)的回調(diào)函數(shù),就執(zhí)行它們坚冀。然后進(jìn)入下個(gè)循環(huán)济赎,如果不再有事件處理,就退出進(jìn)程。

Tick流程圖.png

觀察者——在每個(gè) Tick 的過(guò)程中司训,判斷是否有事件需要處理

每個(gè)事件循環(huán)中有一個(gè)或多個(gè)觀察者构捡,而判斷是否有事件要處理的過(guò)程就是向這些觀察者詢問(wèn)是否有要處理的事件

事件可能來(lái)自用戶的點(diǎn)擊或者加載某些文件時(shí)產(chǎn)生,而產(chǎn)生的事件都有對(duì)應(yīng)的觀察者壳猜。在 Node 中勾徽,事件主要來(lái)源于網(wǎng)絡(luò)請(qǐng)求、文件 I/O 等蓖谢,這些事件對(duì)應(yīng)的觀察者有文件 I/O 觀察者捂蕴、網(wǎng)絡(luò) I/O 觀察者等譬涡。觀察者將事件進(jìn)行了分類闪幽。

事件循環(huán)是一個(gè)典型的生產(chǎn)者/消費(fèi)者模型。異步 I/O涡匀、網(wǎng)絡(luò)請(qǐng)求等則是事件的生產(chǎn)者盯腌,源源不斷為 Node 提供不同類型的事件,這些事件被傳遞到對(duì)應(yīng)的觀察者那里陨瘩,事件循環(huán)則從觀察者那里取出事件并處理腕够。

在 Windows 下,這個(gè)循環(huán)基于 IOCP 創(chuàng)建舌劳,而在 *nix 下則基于多線程創(chuàng)建

請(qǐng)求對(duì)象——從 JavaScript 發(fā)起調(diào)用到內(nèi)核執(zhí)行完 I/O 操作的過(guò)渡過(guò)程中的中間產(chǎn)物

從 JavaScript 調(diào)用 Node 的核心模塊帚湘,核心模塊調(diào)用 C++ 內(nèi)建模塊,內(nèi)建模塊通過(guò) libuv 進(jìn)行系統(tǒng)調(diào)用甚淡,這里的 libuv 作為封裝層大诸,有兩個(gè)平臺(tái)的實(shí)現(xiàn),實(shí)質(zhì)上是調(diào)用了 uv_fs_open() 方法贯卦。在 uv_fs_open() 的調(diào)用過(guò)程中资柔,我們創(chuàng)建了一個(gè) FSReqWrap 請(qǐng)求對(duì)象。從 JavaScript 層傳入的參數(shù)和當(dāng)前方法都被封裝在這個(gè)請(qǐng)求對(duì)象中撵割,其中我們最為關(guān)注的回調(diào)函數(shù)則被設(shè)置在這個(gè)對(duì)象的 oncomplete_sym 屬性上:req_wrap->object->Set(oncomplete_sym, callback);

對(duì)象包裝完畢后贿堰,在 Windows 下,則調(diào)用 QueueUserWorkItem() 方法將這個(gè) FSReqWrap 對(duì)象推入線程池中等待執(zhí)行啡彬。

至此羹与,JavaScript 調(diào)用立即返回, 由 JavaScript 層面發(fā)出的異步調(diào)用的第一階段就此結(jié)束庶灿。JavaScript 線程可以繼續(xù)執(zhí)行當(dāng)前任務(wù)的后續(xù)操作纵搁。當(dāng)前的 I/O 操作在線程池中等待執(zhí)行。不管它是否阻塞 I/O跳仿,都不會(huì)影響到 JavaScript 線程的后續(xù)執(zhí)行诡渴,如此就達(dá)到了異步的目的。

請(qǐng)求對(duì)象是異步 I/O 過(guò)程中的重要中間產(chǎn)物,所有的狀態(tài)都保存在這個(gè)對(duì)象中妄辩,包括送入線程池等待執(zhí)行以及 I/O 操作完畢后的回調(diào)處理

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

線程池中的 I/O 操作調(diào)用完畢之后惑灵,會(huì)將獲取的結(jié)果儲(chǔ)存在 req->result 屬性上,然后調(diào)用 PostQueuedCompletionStatus() 通知 IOCP眼耀,告知當(dāng)前對(duì)象操作已經(jīng)完畢:PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

PostQueuedCompletionStatus() 方法的作用是向 IOCP 提交執(zhí)行狀態(tài)英支,并將線程歸還線程池。

在這個(gè)過(guò)程中哮伟,我們其實(shí)還動(dòng)用了事件循環(huán)的 I/O 觀察者干花。在每次 Tick 的執(zhí)行中,它會(huì)調(diào)用 IOCP 相關(guān)的 FetQueuedCompletionStatus() 方法檢查線程池中是否有執(zhí)行完的請(qǐng)求楞黄,如果存在池凄,會(huì)將請(qǐng)求對(duì)象加入到 I/O 觀察者的隊(duì)列中,然后將其當(dāng)做事件處理鬼廓。

I/O 觀察者回調(diào)函數(shù)的行為就是取出請(qǐng)求對(duì)象的 result 屬性作為參數(shù)肿仑,取出 oncomplete_sym 屬性作為方法,然后調(diào)用執(zhí)行碎税。以此達(dá)到調(diào)用 JavaScript 中傳入的回調(diào)函數(shù)的目的尤慰。

整個(gè)異步IO的流程.png

事件循環(huán)、觀察者雷蹂、請(qǐng)求對(duì)象伟端、I/O 線程池這四者共同構(gòu)成了 Node 異步 I/O 模型的基本要素

在 Node 中,除了 JavaScript 是單線程外匪煌,Node 自身其實(shí)是多線程的责蝠,只是 I/O 線程使用的 CPU 較少。另一個(gè)需要重視的觀點(diǎn)則是虐杯,除了用戶代碼無(wú)法并行執(zhí)行外玛歌,所有的 I/O(磁盤(pán) I/O 和網(wǎng)絡(luò) I/O 等)則是可以并行起來(lái)的。

非 I/O 的異步 API

定時(shí)器

setTimeout() 和 setInterval() 與瀏覽器中的 API 是一致的擎椰,分別用于單次和多次定時(shí)執(zhí)行任務(wù)支子。它們的實(shí)現(xiàn)原理和異步 I/O 比較類似,只是不需要 I/O 線程池的參與达舒。定時(shí)器的問(wèn)題在于值朋,它并非精確的(容忍范圍內(nèi))。盡管事件循環(huán)十分快巩搏,但是如果某一次循環(huán)占用的事件較長(zhǎng)昨登,那么下次循環(huán)時(shí),它也許已經(jīng)超時(shí)很久了贯底。


setTimeout()的行為.png

process.nextTick()

采用定時(shí)器需要?jiǎng)佑眉t黑樹(shù)丰辣,創(chuàng)建定時(shí)器對(duì)象和迭代等操作,而 setTimeout(fn, 0) 的方式較為浪費(fèi)性能。實(shí)際上 process.nextTick() 的方法的操作相對(duì)較為輕量笙什。

每次調(diào)用 process.nextTick() 方法飘哨,只會(huì)將回調(diào)函數(shù)放入隊(duì)列中,在下一輪 Tick 時(shí)取出執(zhí)行琐凭。定時(shí)器中采用紅黑樹(shù)的操作時(shí)間復(fù)雜度為 O(lg(n))芽隆,nextTick() 的時(shí)間復(fù)雜度為 O(1)。相較之下统屈, process.nextTick() 更高效胚吁。

setImmediate()

setImmediate() 方法與 process.nextTick() 方法十分類似,都是將回調(diào)函數(shù)延遲執(zhí)行

區(qū)別是愁憔,process.nextTick() 中的回調(diào)函數(shù)執(zhí)行的優(yōu)先級(jí)要高于 setImmediate()腕扶。這里的原因在于事件循環(huán)對(duì)觀察者的檢查是有先后順序的,process.nextTick() 屬于 idle 觀察者惩淳,setImmediate() 屬于 check 觀察者蕉毯。在每一次輪循環(huán)檢查中,idle 觀察者先于 I/O 觀察者思犁,I/O 觀察者先于 check 觀察者

在具體實(shí)現(xiàn)上,process.nextTick() 的回調(diào)函數(shù)保持在一個(gè)數(shù)組中进肯,setImmediate() 的結(jié)果則是保存在鏈表中激蹲。在行為上,process.nextTick() 在每輪循環(huán)中會(huì)將數(shù)組中的回調(diào)函數(shù)全部執(zhí)行完江掩,而 setImmediate() 在每輪循環(huán)中執(zhí)行鏈表中的一個(gè)回調(diào)函數(shù)学辱。

// 加入兩個(gè)nextTick()de 回調(diào)函數(shù)
process.nextTick(function () {
 console.log('nextTick延遲執(zhí)行1');
});
process.nextTick(function () {
 console.log('nextTick延遲執(zhí)行2');
});
// 加入兩個(gè)setImmediate()的回調(diào)函數(shù)
setImmediate(function () {
 console.log('setImmediate延遲執(zhí)行1');
 // 進(jìn)入下次循環(huán)
 process.nextTick(function () {
 console.log('強(qiáng)勢(shì)插入');
 });
});
setImmediate(function () {
 console.log('setImmediate延遲執(zhí)行2');
});
console.log('正常執(zhí)行');
// 其執(zhí)行結(jié)果如下:
//// 正常執(zhí)行
//// nextTick延遲執(zhí)行1
//// nextTick延遲執(zhí)行2
//// setImmediate延遲執(zhí)行1
//// 強(qiáng)勢(shì)插入
//// setImmediate延遲執(zhí)行2

從執(zhí)行結(jié)果上可以看出,當(dāng)?shù)谝粋€(gè) setImmediate() 的回調(diào)函數(shù)執(zhí)行后环形,并沒(méi)有立即執(zhí)行第二個(gè)策泣,而是進(jìn)入了下一輪循環(huán),再次按 process.nextTick() 優(yōu)先抬吟、setImmediate() 次后的順序執(zhí)行萨咕。之所以這樣設(shè)計(jì),是為了保證每輪循環(huán)能夠較快地執(zhí)行結(jié)束火本,防止 CPU 占用過(guò)多而阻塞后續(xù) I/O 調(diào)用的情況危队。

事件驅(qū)動(dòng)與高性能服務(wù)器

Node 通過(guò)事件驅(qū)動(dòng)的方式處理請(qǐng)求,無(wú)須為每一個(gè)請(qǐng)求創(chuàng)建額外的對(duì)應(yīng)線程钙畔,可以省掉創(chuàng)建線程和銷毀線程的開(kāi)銷茫陆,同時(shí)操作系統(tǒng)在調(diào)度任務(wù)時(shí)因?yàn)榫€程較少,上下文切換的代價(jià)很低擎析。這使得服務(wù)器有條不紊地處理請(qǐng)求簿盅,即使在大量連接的情況下,也不受線程上下文切換開(kāi)銷的影響,這是 Node 高性能的一個(gè)原因桨醋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末见秽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子讨盒,更是在濱河造成了極大的恐慌解取,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件返顺,死亡現(xiàn)場(chǎng)離奇詭異禀苦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)遂鹊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)振乏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人秉扑,你說(shuō)我怎么就攤上這事慧邮。” “怎么了舟陆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵误澳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我秦躯,道長(zhǎng)忆谓,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任踱承,我火速辦了婚禮倡缠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茎活。我一直安慰自己昙沦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布载荔。 她就那樣靜靜地躺著盾饮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪身辨。 梳的紋絲不亂的頭發(fā)上丐谋,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音煌珊,去河邊找鬼号俐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛定庵,可吹牛的內(nèi)容都是我干的吏饿。 我是一名探鬼主播踪危,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼猪落!你這毒婦竟也來(lái)了贞远?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笨忌,失蹤者是張志新(化名)和其女友劉穎蓝仲,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體官疲,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袱结,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了途凫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垢夹。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖维费,靈堂內(nèi)的尸體忽然破棺而出果元,到底是詐尸還是另有隱情,我是刑警寧澤犀盟,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布而晒,位于F島的核電站,受9級(jí)特大地震影響且蓬,放射性物質(zhì)發(fā)生泄漏欣硼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一恶阴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豹障,春花似錦冯事、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至累魔,卻和暖如春摔笤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背垦写。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工吕世, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梯投。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓命辖,卻偏偏與公主長(zhǎng)得像况毅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尔艇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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

  • 1.為什么要使用異步I/O 1.1 用戶體驗(yàn) 瀏覽器中的Javascripts是在單線程上執(zhí)行的尔许,并且和UI渲染公...
    maikuraki閱讀 550評(píng)論 0 0
  • I/O簡(jiǎn)介 1.I/O操作:內(nèi)核在進(jìn)行文件I/O操作時(shí),通過(guò)文件描述符(fd:一個(gè)整數(shù)—應(yīng)用程序和內(nèi)核之間的憑證)...
    勵(lì)志擺脫懶癌的少女醬閱讀 1,700評(píng)論 0 1
  • Node的異步I/O 我們?yōu)槭裁葱枰惒絀/O终娃? 用戶體驗(yàn)服務(wù)器端如果基于同步執(zhí)行的味廊,隨著應(yīng)用復(fù)雜性的增加,響應(yīng)的...
    俗三瘋閱讀 505評(píng)論 0 0
  • 單線程編程會(huì)因阻塞I/O導(dǎo)致硬件資源得不到更優(yōu)的使用棠耕。多線程編程也因?yàn)榫幊讨械乃梨i余佛、狀態(tài)同步等問(wèn)題讓開(kāi)發(fā)人員頭痛。...
    exialym閱讀 441評(píng)論 0 1
  • 異步IO實(shí)現(xiàn)現(xiàn)狀 I/O的阻塞與非阻塞:IO對(duì)于操作系統(tǒng)內(nèi)核而言昧辽,只有阻塞與非阻塞兩種方式衙熔。阻塞模式的I/O會(huì)造成...
    fangPeng__閱讀 1,807評(píng)論 0 0