異步I/O

為什么要異步 I/O

用戶體驗(yàn)

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

資源分配

利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)同步等問題;利用異步 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)用,無須通過遍歷或者事件喚醒等方式輪詢,可以直接處理下一個(gè)任務(wù),只需在 I/O 完成后通過信號(hào)或回調(diào)將數(shù)據(jù)傳遞給應(yīng)用程序即可

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

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


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

我們時(shí)常提到 Node 是單線程的,這里的單線程僅僅只是 JavaScript 執(zhí)行在單線程中罷了。在 Node 中,無論是 *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)體的過程我們成為 Tick。每個(gè) Tick 的過程就是查看是否有事件待處理,如果有,就取出事件及其相關(guān)的回調(diào)函數(shù)。如果存在關(guān)聯(lián)的回調(diào)函數(shù),就執(zhí)行它們。然后進(jìn)入下個(gè)循環(huán),如果不再有事件處理,就退出進(jìn)程。

Tick流程圖.png

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

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

事件可能來自用戶的點(diǎn)擊或者加載某些文件時(shí)產(chǎn)生,而產(chǎn)生的事件都有對(duì)應(yīng)的觀察者。在 Node 中,事件主要來源于網(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 操作的過渡過程中的中間產(chǎn)物

從 JavaScript 調(diào)用 Node 的核心模塊,核心模塊調(diào)用 C++ 內(nèi)建模塊,內(nèi)建模塊通過 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)用過程中,我們創(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 過程中的重要中間產(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è)過程中,我們其實(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)則是,除了用戶代碼無法并行執(zhí)行外,所有的 I/O(磁盤 I/O 和網(wǎng)絡(luò) I/O 等)則是可以并行起來的。

非 I/O 的異步 API

定時(shí)器

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


setTimeout()的行為.png

process.nextTick()

采用定時(shí)器需要?jiǎng)佑眉t黑樹,創(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í)間復(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í)行后,并沒有立即執(zhí)行第二個(gè),而是進(jìn)入了下一輪循環(huán),再次按 process.nextTick() 優(yōu)先、setImmediate() 次后的順序執(zhí)行。之所以這樣設(shè)計(jì),是為了保證每輪循環(huán)能夠較快地執(zhí)行結(jié)束,防止 CPU 占用過多而阻塞后續(xù) I/O 調(diào)用的情況。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容