淺談Node異步IO和事件循環(huán)

前言

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、成都小程序開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了萊蕪免費(fèi)建站歡迎大家使用!

學(xué)習(xí)Node就繞不開異步IO, 異步IO又與事件循環(huán)息息相關(guān), 而關(guān)于這一塊一直沒有仔細(xì)去了解整理過, 剛好最近在做項(xiàng)目的時(shí)候, 有了一些思考就記錄了下來, 希望能盡量將這一塊的知識(shí)整理清楚, 如有錯(cuò)誤, 請(qǐng)指點(diǎn)輕噴~~

一些概念

 同步異步 & 阻塞非阻塞

查閱資料的時(shí)候, 發(fā)現(xiàn)很多人都對(duì) 異步和非阻塞 的概念有點(diǎn)混淆, 其實(shí)兩者是完全不同的, 同步異步指的是 行為即兩者之間的關(guān)系 , 而阻塞非阻塞指的是 狀態(tài)即某一方 。

以前端請(qǐng)求為一個(gè)例子,下面的代碼很多人都應(yīng)該寫過

$.ajax(url).succedd(() => {
 ......
 // to do something
})

同步異步

如果是同步的話, 那么應(yīng)該是client發(fā)起請(qǐng)求后, 一直等到serve處理請(qǐng)求完成后才返回繼續(xù)執(zhí)行后續(xù)的邏輯, 這樣 client和serve之間就保持了同步的狀態(tài)

如果是異步的話, 那么應(yīng)該是client發(fā)起請(qǐng)求后, 立即返回 , 而請(qǐng)求可能還沒有到達(dá)server端或者請(qǐng)求正在處理, 當(dāng)然在異步情況下, client端通常會(huì)注冊(cè)事件來處理請(qǐng)求完成后的情況, 如上面的succeed函數(shù)。

阻塞非阻塞

首先需要明白一個(gè)概念, Js是單線程, 但是瀏覽器并不是, 事實(shí)上你的請(qǐng)求是瀏覽器的另一個(gè)線程在跑。

如果是阻塞的話, 那么 該線程就會(huì)一直等到這個(gè)請(qǐng)求完成之后才能被釋放用于其他請(qǐng)求 。

如果是非阻塞的話, 那么 該線程就可以發(fā)起請(qǐng)求后而不用等請(qǐng)求完成繼續(xù)做其他事情

總結(jié)

之所以經(jīng)常會(huì)混亂是因?yàn)闆]有說清楚討論的是哪一部分(下面會(huì)提到), 所以 同步異步討論的對(duì)象是雙方, 而阻塞非阻塞討論的對(duì)象是自身

IO和CPU

Io和Cpu是可以同時(shí)進(jìn)行工作的 。

IO:

I/O(英語:Input/Output),即輸入/輸出,通常指數(shù)據(jù)在內(nèi)部存儲(chǔ)器和外部存儲(chǔ)器或其他周邊設(shè)備之間的輸入和輸出。

cpu

解釋計(jì)算機(jī)指令以及處理計(jì)算機(jī)軟件中的數(shù)據(jù)。

Node中的異步IO模型

IO分為 磁盤IO和網(wǎng)絡(luò)IO , 其具有兩個(gè)步驟

  1. 等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)
  2. 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)

Node中的磁盤Io

以下的討論基于*nix系統(tǒng)。

理想的異步Io應(yīng)該像上面討論的一樣, 如圖:

淺談Node 異步IO和事件循環(huán)

而實(shí)際上, 我們的系統(tǒng)并不能完美的實(shí)現(xiàn)這樣的一種調(diào)用方式, Node的異步IO, 如讀取文件等采用的是線程池的方式來實(shí)現(xiàn), 可以看到, Node通過另外一個(gè)線程來進(jìn)行Io操作, 完成后再通知主線程:

淺談Node 異步IO和事件循環(huán)

而在window下, 則是利用 IOCP 接口來完成, IOCP從用戶的角度來說確實(shí)是完美的異步調(diào)用方式, 而實(shí)際也是利用內(nèi)核中的線程池, 其與nix系統(tǒng)的不同在于后者的線程池是用戶層提供的線程池。

Node中的網(wǎng)絡(luò)Io

在進(jìn)入主題之前, 我們先了解下Linux的Io模式, 這里推薦大家看這篇文章, 大致總結(jié)如下:

阻塞 I/O(blocking IO)

淺談Node 異步IO和事件循環(huán)

所以,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了。

非阻塞 I/O(nonblocking IO)

淺談Node 異步IO和事件循環(huán)

當(dāng)用戶進(jìn)程發(fā)出read操作時(shí),如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好,那么它并不會(huì)block用戶進(jìn)程,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講 ,它發(fā)起一個(gè)read操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí),它就知道數(shù)據(jù)還沒有準(zhǔn)備好,于是它可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。

I/O 多路復(fù)用( IO multiplexing)

淺談Node 異步IO和事件循環(huán)

所以,I/O 多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回。

異步 I/O(asynchronous IO)

淺談Node 異步IO和事件循環(huán)

用戶進(jìn)程發(fā)起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當(dāng)它受到一個(gè)asynchronous read之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal,告訴它read操作完成了。

而在Node中, 采用的是I/O 多路復(fù)用的模式, 而在I/O多路復(fù)用的模式中, 又具有read, select, poll, epoll等幾個(gè)子模式, Node采用的是最優(yōu)的epoll模式, 這里簡單說下其中的區(qū)別, 并且解釋下為什么epoll是最優(yōu)的。

read

read。它是一種最原始、性能最低的一種,它會(huì)重復(fù)檢查I/O的狀態(tài)來完成數(shù)據(jù)的完整讀取。在得到最終數(shù)據(jù)前,CPU一直耗用在I/O狀態(tài)的重復(fù)檢查上。圖1是通過read進(jìn)行輪詢的示意圖。

淺談Node 異步IO和事件循環(huán)

select

select。它是在read的基礎(chǔ)上改進(jìn)的一種方案,通過對(duì)文件描述符上的事件狀態(tài)進(jìn)行判斷。圖2是通過select進(jìn)行輪詢的示意圖。select輪詢具有一個(gè)較弱的限制,那就是由于它采用一個(gè)1024長度的數(shù)組來存儲(chǔ)狀態(tài),也就是說它最多可以同時(shí)檢查1024個(gè)文件描述符。

淺談Node 異步IO和事件循環(huán)

poll

poll。poll比select有所改進(jìn),采用鏈表的方式避免數(shù)組長度的限制,其次它可以避免不必要的檢查。但是文件描述符較多的時(shí)候,它的性能是十分低下的。

淺談Node 異步IO和事件循環(huán)

epoll

該方案是Linux下效率最高的I/O事件通知機(jī)制,在進(jìn)入輪詢的時(shí)候如果沒有檢查到I/O事件,將會(huì)進(jìn)行休眠,直到事件發(fā)生將它喚醒。它是真實(shí)利用了事件通知,執(zhí)行回調(diào)的方式,而不是遍歷查詢,所以不會(huì)浪費(fèi)CPU,執(zhí)行效率較高。

淺談Node 異步IO和事件循環(huán)

除此之外, 另外的poll和select還具有以下的缺點(diǎn)(引用自 文章 ):

  • 每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會(huì)很大
  • 同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
  • select支持的文件描述符數(shù)量太小了,默認(rèn)是1024

epoll對(duì)于上述的改進(jìn)

epoll既然是對(duì)select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊(cè)要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。

對(duì)于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過程中只會(huì)拷貝一次。

對(duì)于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。

對(duì)于第三個(gè)缺點(diǎn),epoll沒有這個(gè)限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

Node中的異步網(wǎng)絡(luò)Io就是利用了epoll來實(shí)現(xiàn), 簡單來說, 就是利用一個(gè)線程來管理眾多的IO請(qǐng)求, 通過事件機(jī)制實(shí)現(xiàn)消息通訊。

事件循環(huán)

理解了Node中磁盤IO和網(wǎng)絡(luò)IO的底層實(shí)現(xiàn)后, 基于上面的代碼, 可以看出Node是基于事件注冊(cè)的方式在完成Io后進(jìn)行一系列的處理, 其內(nèi)部是利用了事件循環(huán)的機(jī)制。

關(guān)于事件循環(huán), 是指JS在每次執(zhí)行完同步任務(wù)后會(huì)檢查執(zhí)行棧是否為空, 是的話就會(huì)去執(zhí)行注冊(cè)的事件列表, 不斷的循環(huán)該過程。Node中的事件循環(huán)有六個(gè)階段:

淺談Node 異步IO和事件循環(huán)

其中的每個(gè)階段都會(huì)處理相關(guān)的事件:

  • timers: 執(zhí)行setTimeout和setInterval中到期的callback。
  • pending callback: 執(zhí)行延遲到下一個(gè)循環(huán)迭代的 I/O 回調(diào)。
  • idle, prepare:僅系統(tǒng)內(nèi)部使用。
  • poll:檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下,除了關(guān)閉的回調(diào)函數(shù),它們由計(jì)時(shí)器和 setImmediate() 排定的之外),其余情況 node 將在此處阻塞。(即本文的內(nèi)容相關(guān)))
  • check: setImmediate() 回調(diào)函數(shù)在這里執(zhí)行。
  • close callbacks: 執(zhí)行close事件的callback,例如socket.on('close'[,fn])或者h(yuǎn)ttp.server.on('close, fn)。

ok, 這樣就解釋了Node是如何執(zhí)行我們注冊(cè)的事件, 那么還缺少一個(gè)環(huán)節(jié), Node又是怎么把事件和IO請(qǐng)求對(duì)應(yīng)起來呢? 這里涉及到了另外一種中間產(chǎn)物請(qǐng)求對(duì)象。

以打開一個(gè)文件為例子:

fs.open = function(path, flags, mode, callback){

//...

binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback);

}

fs.open()的作用是根據(jù)指定路徑和參數(shù)去打開一個(gè)文件,從而得到一個(gè)文件描述符,這是后續(xù)所有I/O操作的初始操作。從前面的代碼中可以看到,JavaScript層面的代碼通過調(diào)用C++核心模塊進(jìn)行下層的操作。

淺談Node 異步IO和事件循環(huán)

從JavaScript調(diào)用Node的核心模塊,核心模塊調(diào)用C++內(nèi)建模塊,內(nèi)建模塊通過libuv進(jìn)行系統(tǒng)調(diào)用,這是Node里經(jī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);

QueueUserWorkItem()方法接受3個(gè)參數(shù):第一個(gè)參數(shù)是將要執(zhí)行的方法的引用,這里引用的uv_fs_thread_proc;第二個(gè)參數(shù)是uv_fs_thread_proc方法運(yùn)行時(shí)所需要的參數(shù);第三個(gè)參數(shù)是執(zhí)行的標(biāo)志。當(dāng)線程池中有可用線程時(shí),我們會(huì)調(diào)用uv_fs_thread_proc()方法。uv_fs_thread_proc()方法會(huì)根據(jù)傳入?yún)?shù)的類型調(diào)用相應(yīng)的底層函數(shù)。以u(píng)v_fs_open()為例,實(shí)際上調(diào)用fs_open()方法。

至此,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)處理。

關(guān)于這一塊其實(shí)個(gè)人認(rèn)為不用過于細(xì)究, 大致上知道有這么一個(gè)請(qǐng)求對(duì)象即可, 最后總結(jié)一下整個(gè)異步IO的流程:

淺談Node 異步IO和事件循環(huán)

圖引用自深入淺出NodeJs

至此, Node的整個(gè)異步Io流程都已經(jīng)清晰了, 它是依賴于IO線程池epoll、事件循環(huán)、請(qǐng)求對(duì)象共同構(gòu)成的一個(gè)管理機(jī)制。

Node為什么更適合IO密集

Node為人津津樂道的就是它更適合 IO密集型 的系統(tǒng), 并且具有 更好的性能 , 關(guān)于這一點(diǎn)其實(shí)與它的異步IO息息相關(guān)。

對(duì)于一個(gè)request而言, 如果我們依賴io的結(jié)果, 異步io和同步阻塞io(每線程/每請(qǐng)求)都是要等到io完成才能繼續(xù)執(zhí)行. 而同步阻塞io, 一旦阻塞就不會(huì)在獲得cpu時(shí)間片, 那么為什么異步的性能更好呢?

其根本原因在于同步阻塞Io需要為 每一個(gè)請(qǐng)求創(chuàng)建一個(gè)線程 , 在Io的時(shí)候, 線程被block, 雖然不消耗cpu, 但是其本身具有內(nèi)存開銷, 當(dāng)大并發(fā)的請(qǐng)求到來時(shí), 內(nèi)存很快被用光, 導(dǎo)致服務(wù)器緩慢 , 在加上, 切換上下文代價(jià)也會(huì)消耗cpu資源 。而Node的異步Io是通過事件機(jī)制來處理的, 它不需要為每一個(gè)請(qǐng)求創(chuàng)建一個(gè)線程, 這就是為什么Node的性能更高。

特別是在Web這種IO密集型的情形下更具優(yōu)勢(shì), 除開Node之外, 其實(shí)還有另外一種事件機(jī)制的服務(wù)器Ngnix, 如果明白了Node的機(jī)制對(duì)于Ngnix應(yīng)該會(huì)很容易理解, 有興趣的話推薦看這篇文章。

總結(jié)

在真正的學(xué)習(xí)Node異步IO之前, 經(jīng)??吹揭恍╆P(guān)于Node適不適合作為服務(wù)器端的開發(fā)語言的爭論, 當(dāng)然也有很多片面的說法。

其實(shí), 關(guān)于這個(gè)問題還是取決于你的業(yè)務(wù)場景。

假設(shè)你的業(yè)務(wù)是cpu密集型的, 那你采用Node來開發(fā), 肯定是不適合的。 為什么不適合? 因?yàn)镹ode是單線程, 你被阻塞在計(jì)算的時(shí)候, 其他的事件就做不了, 處理不了請(qǐng)求, 也處理不了回調(diào)。

那么在IO密集型中, Node就比Java好嗎? 其實(shí)也不一定, 還是要取決于你的業(yè)務(wù)。 如果你的業(yè)務(wù)是非常大的并發(fā), 但是你的服務(wù)器資源又有限, 就好比現(xiàn)在有個(gè)入口, Node可以一次進(jìn)10個(gè)人, 而Java依次排隊(duì)進(jìn)一個(gè)人, 如果是10個(gè)人同時(shí)進(jìn), 當(dāng)然是Node更具有優(yōu)勢(shì), 但是假設(shè)有100個(gè)人(如1w個(gè)異步請(qǐng)求之類)的話, 那么Node就會(huì)因?yàn)樗漠惒綑C(jī)制導(dǎo)致應(yīng)用被掛起,內(nèi)存狂飆,IO堵塞,而且不可恢復(fù),這個(gè)時(shí)候你只能重啟了。而Java卻可以有序的處理, 雖然會(huì)慢一點(diǎn)。 而一臺(tái)服務(wù)器掛了造成的線上事故的損失更是不可衡量的。(當(dāng)然, 如果服務(wù)器資源足夠的話, Node也能處理)。

最后, 事實(shí)上Java也是具有異步IO的庫, 只是相對(duì)來說, Node的語法更自然更貼近, 也就更適合。

以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。

分享標(biāo)題:淺談Node異步IO和事件循環(huán)
網(wǎng)站路徑:http://muchs.cn/article8/jpgsip.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設(shè)計(jì)公司、營銷型網(wǎng)站建設(shè)、網(wǎng)站建設(shè)、虛擬主機(jī)品牌網(wǎng)站建設(shè)、網(wǎng)站維護(hù)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設(shè)