在學(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ǎng)上查了一些文章寨昙,大概明白了一點(diǎn),便進(jìn)行整理尚卫,方便以后查看。
場(chǎng)景模擬
我們來模擬一個(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)姆蘸。示例圖如下:
在這個(gè)模型中芙委,會(huì)經(jīng)過三個(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 可以用來客觀描述服務(wù)器的并發(fā)能力膀篮,QPS 越大岂膳,服務(wù)器的并發(fā)能力越好。
單核單線程的情況
假定我們的服務(wù)器是單核單線程的谈截,那么其處理情況如下所示:
服務(wù)器收到了兩個(gè)請(qǐng)求:請(qǐng)求1和請(qǐng)求2。處理流程如下:
- 收到請(qǐng)求1燎潮,進(jìn)行處理
- 讀取數(shù)據(jù)庫(kù)扼倘,由于是單線程,CPU 需要等待 IO 讀取完成再進(jìn)行后面的操作
- 處理讀取到的數(shù)據(jù)爪喘,響應(yīng)客戶端
- 按上面的流程繼續(xù)處理請(qǐng)求2
單線程的瓶頸在于無法充分利用 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è)線程洒敏,其處理情況如下所示:
采用單核多線程時(shí)凶伙,情況就不一樣了函荣,對(duì)于請(qǐng)求1和請(qǐng)求2链韭,將由兩個(gè)不同的線程進(jìn)行處理敞峭,流程如下:
- (線程1)收到請(qǐng)求1的請(qǐng)求進(jìn)行處理
- (線程1)請(qǐng)求1開始 IO 讀取旋讹,CPU 空閑
- (線程2)CPU 切換到請(qǐng)求2所在的線程,并進(jìn)行請(qǐng)求處理
- (線程2)請(qǐng)求2開始 IO 讀取沉迹,CPU 空閑
- (線程1)請(qǐng)求1的 IO 讀取完畢鞭呕,CPU 切換到請(qǐng)求1所在的線程宛官,處理數(shù)據(jù)并響應(yīng)客戶端,CPU 空閑
- (線程2)請(qǐng)求2的 IO 讀取完畢腋么,CPU 切換到請(qǐng)求2所在的線程亥揖,處理數(shù)據(jù)并響應(yīng)客戶端,CPU 空閑
- 按照上面的流程進(jìn)行請(qǐng)求3和請(qǐng)求4的處理
在多線程的模型下摧扇,由于 CPU 不用等待 IO 讀取完成扛稽,其核心得到了充分的利用庇绽,在這個(gè)模型下橙困,處理完兩個(gè)請(qǐng)求所耗時(shí)為 4ms耕餐,平均處理一個(gè)請(qǐng)求所耗時(shí)為 2ms肠缔,QPS 為 500哼转,并發(fā)能力比單線程模型好多了壹蔓。
多線程模型的問題
多線程模型最大的問題佣蓉,莫過于線程上下文切換所帶來的額外開銷了勇凭。上面展示的情況,是沒有上下文切換下的理想情況义辕,但在真實(shí)的環(huán)境下虾标,兩個(gè)線程間進(jìn)行切換,必定會(huì)產(chǎn)生開銷的灌砖,而且還不小璧函。因此上面的處理情況可能是這樣:
因此,由于頻繁上下文切換造成的開銷基显,上面的多線程模型的并發(fā)能力比理想情況下要弱柳譬。
單線程異步 IO 的情況
在多線程模型中,通過使用線程切換续镇,避免了 CPU 因等待 IO 操作的閑置美澳,最大程度上利用了 CPU 資源,但是線程間的頻繁上下文切換也會(huì)產(chǎn)生很大的開銷摸航,同樣會(huì)增加服務(wù)器的壓力制跟。因此,要想避免線程上下文切換的帶來的開銷雨膨,就只有使用單線程。
在使用單線程的情況下排监,能否實(shí)現(xiàn)非阻塞的 IO 呢?
上面就是一個(gè)單線程非阻塞 IO 的模型谷暮,處理請(qǐng)求時(shí)的流程如下:
- 收到請(qǐng)求1腾夯,開始處理請(qǐng)求
- 進(jìn)行請(qǐng)求1的 IO 讀取竟秫,并注冊(cè)一個(gè)回調(diào)函數(shù)(處理數(shù)據(jù)并響應(yīng)客戶端),同時(shí)線程不阻塞馒稍,繼續(xù)處理請(qǐng)求2
- 進(jìn)行請(qǐng)求2的 IO 讀取如输,并注冊(cè)一個(gè)回調(diào)函數(shù)(處理數(shù)據(jù)并響應(yīng)客戶端)澳化,同時(shí)線程不阻塞,繼續(xù)處理剩下的請(qǐng)求
- 請(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)行,沒有頻繁上下文切換帶來的開銷鹅心,因此這個(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
完叉趣。