為什么 Event Loop 適合處理高并發(fā)?

在學(xué)習(xí) Node 的時(shí)候,一定會(huì)被告知 Node 是基于 Event Loop 的,以及事件驅(qū)動(dòng)、事件隊(duì)列、非阻塞 IO 等概念,最終得出一個(gè)結(jié)論:Node 非常適合 IO 密集型的應(yīng)用,能夠以很少的資源消耗實(shí)現(xiàn)高并發(fā)。
但為什么 Event Loop 架構(gòu)可以實(shí)現(xiàn)較高的并發(fā)呢?這個(gè)問(wèn)題我一直也不明白,于是我在網(wǎng)上查了一些文章,大概明白了一點(diǎn),便進(jìn)行整理,方便以后查看。

場(chǎng)景模擬

我們來(lái)模擬一個(gè)典型的請(qǐng)求-響應(yīng)模型:客戶端向服務(wù)器發(fā)起請(qǐng)求,服務(wù)端收到請(qǐng)求后對(duì)請(qǐng)求進(jìn)行處理,然后進(jìn)行數(shù)據(jù)庫(kù)讀取,最后將讀取的結(jié)果進(jìn)行響應(yīng)。示例圖如下:


請(qǐng)求-響應(yīng)模型.png

在這個(gè)模型中,會(huì)經(jīng)過(guò)三個(gè)階段:

  • 服務(wù)器收到請(qǐng)求并處理
  • 讀取數(shù)據(jù)庫(kù)
  • 服務(wù)器處理數(shù)據(jù)并響應(yīng)

在這三個(gè)階段中,第一個(gè)階段和最后一個(gè)階段會(huì)由 CPU 進(jìn)行計(jì)算,第二個(gè)階段則是 IO 操作,只占用極少的 CPU。
我們假定這三個(gè)階段各耗時(shí) 1ms,因此服務(wù)器處理每個(gè)請(qǐng)求所花的時(shí)間就為 3ms。
假如我們的服務(wù)器是單核 CPU,并只有一個(gè)線程,那么每秒鐘可以處理的請(qǐng)求約等于 333 個(gè),也就是服務(wù)器的 QPS 等于 333。
QPS 可以用來(lái)客觀描述服務(wù)器的并發(fā)能力,QPS 越大,服務(wù)器的并發(fā)能力越好。

單核單線程的情況

假定我們的服務(wù)器是單核單線程的,那么其處理情況如下所示:


單核單線程.png

服務(wù)器收到了兩個(gè)請(qǐng)求:請(qǐng)求1和請(qǐng)求2。處理流程如下:

  1. 收到請(qǐng)求1,進(jìn)行處理
  2. 讀取數(shù)據(jù)庫(kù),由于是單線程,CPU 需要等待 IO 讀取完成再進(jìn)行后面的操作
  3. 處理讀取到的數(shù)據(jù),響應(yīng)客戶端
  4. 按上面的流程繼續(xù)處理請(qǐng)求2

單線程的瓶頸在于無(wú)法充分利用 CPU 資源,在進(jìn)行 IO 讀取時(shí),CPU 實(shí)際上是處于空閑狀態(tài),必須等待 IO 讀取完成再進(jìn)行后面的處理。對(duì)于每個(gè)請(qǐng)求,CPU 都會(huì)有一個(gè)較大的等待時(shí)期。在單線程模型下,服務(wù)器的 QPS 為 333。

單核多線程的情況

假定我們服務(wù)器是單核 CPU,但是開啟了兩個(gè)線程,其處理情況如下所示:


單核多線程.png

采用單核多線程時(shí),情況就不一樣了,對(duì)于請(qǐng)求1和請(qǐng)求2,將由兩個(gè)不同的線程進(jìn)行處理,流程如下:

  1. (線程1)收到請(qǐng)求1的請(qǐng)求進(jìn)行處理
  2. (線程1)請(qǐng)求1開始 IO 讀取,CPU 空閑
  3. (線程2)CPU 切換到請(qǐng)求2所在的線程,并進(jìn)行請(qǐng)求處理
  4. (線程2)請(qǐng)求2開始 IO 讀取,CPU 空閑
  5. (線程1)請(qǐng)求1的 IO 讀取完畢,CPU 切換到請(qǐng)求1所在的線程,處理數(shù)據(jù)并響應(yīng)客戶端,CPU 空閑
  6. (線程2)請(qǐng)求2的 IO 讀取完畢,CPU 切換到請(qǐng)求2所在的線程,處理數(shù)據(jù)并響應(yīng)客戶端,CPU 空閑
  7. 按照上面的流程進(jìn)行請(qǐng)求3和請(qǐng)求4的處理

在多線程的模型下,由于 CPU 不用等待 IO 讀取完成,其核心得到了充分的利用,在這個(gè)模型下,處理完兩個(gè)請(qǐng)求所耗時(shí)為 4ms,平均處理一個(gè)請(qǐng)求所耗時(shí)為 2ms,QPS 為 500,并發(fā)能力比單線程模型好多了。

多線程模型的問(wèn)題

多線程模型最大的問(wèn)題,莫過(guò)于線程上下文切換所帶來(lái)的額外開銷了。上面展示的情況,是沒有上下文切換下的理想情況,但在真實(shí)的環(huán)境下,兩個(gè)線程間進(jìn)行切換,必定會(huì)產(chǎn)生開銷的,而且還不小。因此上面的處理情況可能是這樣:


上下文切換開銷.png

因此,由于頻繁上下文切換造成的開銷,上面的多線程模型的并發(fā)能力比理想情況下要弱。

單線程異步 IO 的情況

在多線程模型中,通過(guò)使用線程切換,避免了 CPU 因等待 IO 操作的閑置,最大程度上利用了 CPU 資源,但是線程間的頻繁上下文切換也會(huì)產(chǎn)生很大的開銷,同樣會(huì)增加服務(wù)器的壓力。因此,要想避免線程上下文切換的帶來(lái)的開銷,就只有使用單線程。
在使用單線程的情況下,能否實(shí)現(xiàn)非阻塞的 IO 呢?


單線程異步IO.png

上面就是一個(gè)單線程非阻塞 IO 的模型,處理請(qǐng)求時(shí)的流程如下:

  1. 收到請(qǐng)求1,開始處理請(qǐng)求
  2. 進(jìn)行請(qǐng)求1的 IO 讀取,并注冊(cè)一個(gè)回調(diào)函數(shù)(處理數(shù)據(jù)并響應(yīng)客戶端),同時(shí)線程不阻塞,繼續(xù)處理請(qǐng)求2
  3. 進(jìn)行請(qǐng)求2的 IO 讀取,并注冊(cè)一個(gè)回調(diào)函數(shù)(處理數(shù)據(jù)并響應(yīng)客戶端),同時(shí)線程不阻塞,繼續(xù)處理剩下的請(qǐng)求
  4. 請(qǐng)求處理結(jié)束后,依次執(zhí)行 IO 讀取是注冊(cè)的回調(diào)函數(shù)(處理數(shù)據(jù)并響應(yīng)客戶端),完成處理

假設(shè)服務(wù)器只接受到兩個(gè)請(qǐng)求:請(qǐng)求1和請(qǐng)求2,按照上面的流程圖,處理這兩個(gè)請(qǐng)求的時(shí)間為 4ms,平均每個(gè)請(qǐng)求用時(shí) 2ms,此時(shí)服務(wù)器的 QPS 為 500。由于是單線程運(yùn)行,沒有頻繁上下文切換帶來(lái)的開銷,因此這個(gè)單線程異步 IO 的模型比多線程模型占用的資源更少,對(duì)服務(wù)器配置要求更低。同時(shí),從流程圖也可以看到,這種架構(gòu)具備很大的吞吐能力,十分適合 IO 密集型的應(yīng)用。

總結(jié)

本文對(duì)幾種常見的任務(wù)處理模型:?jiǎn)尉€程模型、多線程模型、單線程非阻塞 IO 模型進(jìn)行了對(duì)比,依據(jù)是應(yīng)用這些模型時(shí)的服務(wù)器 QPS 值,得出的結(jié)論是單線程非阻塞 IO 模型具備較強(qiáng)的并發(fā)處理能力,且占用更少的資源。
Node 的 Event Loop 基于這種單線程非阻塞 IO 模型,因此具備強(qiáng)大的并發(fā)能力,適合 IO 密集型的應(yīng)用(如游戲、電商秒殺活動(dòng)等)。

附:參考資料
Node.js為什么快
JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop
【樸靈評(píng)注】JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop
理解Node.js的event loop

完。

最后編輯于
?著作權(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ù)。

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