加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長(zhǎng)期合作伙伴
立即加入
  • 正文
    • 參考資料
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

原來(lái) 8 張圖,就能學(xué)廢 Reactor 和 Proactor

2021/04/28
168
閱讀需 28 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

小林,來(lái)了。

這次就來(lái)圖解 Reactor 和 Proactor 這兩個(gè)高性能網(wǎng)絡(luò)模式。

別小看這兩個(gè)東西,特別是 Reactor 模式,市面上常見(jiàn)的開(kāi)源軟件很多都采用了這個(gè)方案,比如 Redis、Nginx、Netty 等等,所以學(xué)好這個(gè)模式設(shè)計(jì)的思想,不僅有助于我們理解很多開(kāi)源軟件,而且也能在面試時(shí)吹逼。

發(fā)車!

 

演進(jìn)

如果要讓服務(wù)器服務(wù)多個(gè)客戶端,那么最直接的方式就是為每一條連接創(chuàng)建線程。

其實(shí)創(chuàng)建進(jìn)程也是可以的,原理是一樣的,進(jìn)程和線程的區(qū)別在于線程比較輕量級(jí)些,線程的創(chuàng)建和線程間切換的成本要小些,為了描述簡(jiǎn)述,后面都以線程為例。

處理完業(yè)務(wù)邏輯后,隨著連接關(guān)閉后線程也同樣要銷毀了,但是這樣不停地創(chuàng)建和銷毀線程,不僅會(huì)帶來(lái)性能開(kāi)銷,也會(huì)造成浪費(fèi)資源,而且如果要連接幾萬(wàn)條連接,創(chuàng)建幾萬(wàn)個(gè)線程去應(yīng)對(duì)也是不現(xiàn)實(shí)的。

要這么解決這個(gè)問(wèn)題呢?我們可以使用「資源復(fù)用」的方式。

也就是不用再為每個(gè)連接創(chuàng)建線程,而是創(chuàng)建一個(gè)「線程池」,將連接分配給線程,然后一個(gè)線程可以處理多個(gè)連接的業(yè)務(wù)。

不過(guò),這樣又引來(lái)一個(gè)新的問(wèn)題,線程怎樣才能高效地處理多個(gè)連接的業(yè)務(wù)?

當(dāng)一個(gè)連接對(duì)應(yīng)一個(gè)線程時(shí),線程一般采用「read -> 業(yè)務(wù)處理 -> send」的處理流程,如果當(dāng)前連接沒(méi)有數(shù)據(jù)可讀,那么線程會(huì)阻塞在 read 操作上( socket 默認(rèn)情況是阻塞 I/O),不過(guò)這種阻塞方式并不影響其他線程。

但是引入了線程池,那么一個(gè)線程要處理多個(gè)連接的業(yè)務(wù),線程在處理某個(gè)連接的 read 操作時(shí),如果遇到?jīng)]有數(shù)據(jù)可讀,就會(huì)發(fā)生阻塞,那么線程就沒(méi)辦法繼續(xù)處理其他連接的業(yè)務(wù)。

要解決這一個(gè)問(wèn)題,最簡(jiǎn)單的方式就是將 socket 改成非阻塞,然后線程不斷地輪詢調(diào)用 read 操作來(lái)判斷是否有數(shù)據(jù),這種方式雖然該能夠解決阻塞的問(wèn)題,但是解決的方式比較粗暴,因?yàn)檩喸兪且?CPU 的,而且隨著一個(gè) 線程處理的連接越多,輪詢的效率就會(huì)越低。

上面的問(wèn)題在于,線程并不知道當(dāng)前連接是否有數(shù)據(jù)可讀,從而需要每次通過(guò) read 去試探。

那有沒(méi)有辦法在只有當(dāng)連接上有數(shù)據(jù)的時(shí)候,線程才去發(fā)起讀請(qǐng)求呢?答案是有的,實(shí)現(xiàn)這一技術(shù)的就是 I/O 多路復(fù)用。

I/O 多路復(fù)用技術(shù)會(huì)用一個(gè)系統(tǒng)調(diào)用函數(shù)來(lái)監(jiān)聽(tīng)我們所有關(guān)心的連接,也就說(shuō)可以在一個(gè)監(jiān)控線程里面監(jiān)控很多的連接。

我們熟悉的 select/poll/epoll 就是內(nèi)核提供給用戶態(tài)的多路復(fù)用系統(tǒng)調(diào)用,線程可以通過(guò)一個(gè)系統(tǒng)調(diào)用函數(shù)從內(nèi)核中獲取多個(gè)事件。

PS:如果想知道 select/poll/epoll 的區(qū)別,可以看看小林之前寫的這篇文章:這次答應(yīng)我,一舉拿下 I/O 多路復(fù)用!

select/poll/epoll 是如何獲取網(wǎng)絡(luò)事件的呢?

在獲取事件時(shí),先把我們要關(guān)心的連接傳給內(nèi)核,再由內(nèi)核檢測(cè):

如果沒(méi)有事件發(fā)生,線程只需阻塞在這個(gè)系統(tǒng)調(diào)用,而無(wú)需像前面的線程池方案那樣輪訓(xùn)調(diào)用 read 操作來(lái)判斷是否有數(shù)據(jù)。

如果有事件發(fā)生,內(nèi)核會(huì)返回產(chǎn)生了事件的連接,線程就會(huì)從阻塞狀態(tài)返回,然后在用戶態(tài)中再處理這些連接對(duì)應(yīng)的業(yè)務(wù)即可。

當(dāng)下開(kāi)源軟件能做到網(wǎng)絡(luò)高性能的原因就是 I/O 多路復(fù)用嗎?

是的,基本是基于 I/O 多路復(fù)用,用過(guò) I/O 多路復(fù)用接口寫網(wǎng)絡(luò)程序的同學(xué),肯定知道是面向過(guò)程的方式寫代碼的,這樣的開(kāi)發(fā)的效率不高。

于是,大佬們基于面向?qū)ο蟮乃枷?,?duì) I/O 多路復(fù)用作了一層封裝,讓使用者不用考慮底層網(wǎng)絡(luò) API 的細(xì)節(jié),只需要關(guān)注應(yīng)用代碼的編寫。

大佬們還為這種模式取了個(gè)讓人第一時(shí)間難以理解的名字:Reactor 模式。

Reactor 翻譯過(guò)來(lái)的意思是「反應(yīng)堆」,可能大家會(huì)聯(lián)想到物理學(xué)里的核反應(yīng)堆,實(shí)際上并不是的這個(gè)意思。

這里的反應(yīng)指的是「對(duì)事件反應(yīng)」,也就是來(lái)了一個(gè)事件,Reactor 就有相對(duì)應(yīng)的反應(yīng)/響應(yīng)。

事實(shí)上,Reactor 模式也叫 Dispatcher 模式,我覺(jué)得這個(gè)名字更貼合該模式的含義,即 I/O 多路復(fù)用監(jiān)聽(tīng)事件,收到事件后,根據(jù)事件類型分配(Dispatch)給某個(gè)進(jìn)程 / 線程。

Reactor 模式主要由 Reactor 和處理資源池這兩個(gè)核心部分組成,它倆負(fù)責(zé)的事情如下:

Reactor 負(fù)責(zé)監(jiān)聽(tīng)和分發(fā)事件,事件類型包含連接事件、讀寫事件;

處理資源池負(fù)責(zé)處理事件,如 read -> 業(yè)務(wù)邏輯 -> send;

Reactor 模式是靈活多變的,可以應(yīng)對(duì)不同的業(yè)務(wù)場(chǎng)景,靈活在于:

Reactor 的數(shù)量可以只有一個(gè),也可以有多個(gè);

處理資源池可以是單個(gè)進(jìn)程 / 線程,也可以是多個(gè)進(jìn)程 /線程;

將上面的兩個(gè)因素排列組設(shè)一下,理論上就可以有 4 種方案選擇:

單 Reactor 單進(jìn)程 / 線程;

單 Reactor 多進(jìn)程 / 線程;

多 Reactor 單進(jìn)程 / 線程;

多 Reactor 多進(jìn)程 / 線程;

其中,「多 Reactor 單進(jìn)程 / 線程」實(shí)現(xiàn)方案相比「單 Reactor 單進(jìn)程 / 線程」方案,不僅復(fù)雜而且也沒(méi)有性能優(yōu)勢(shì),因此實(shí)際中并沒(méi)有應(yīng)用。

剩下的 3 個(gè)方案都是比較經(jīng)典的,且都有應(yīng)用在實(shí)際的項(xiàng)目中:

單 Reactor 單進(jìn)程 / 線程;

單 Reactor 多線程 / 進(jìn)程;

多 Reactor 多進(jìn)程 / 線程;

方案具體使用進(jìn)程還是線程,要看使用的編程語(yǔ)言以及平臺(tái)有關(guān):

Java 語(yǔ)言一般使用線程,比如 Netty;

C 語(yǔ)言使用進(jìn)程和線程都可以,例如 Nginx 使用的是進(jìn)程,Memcache 使用的是線程。

接下來(lái),分別介紹這三個(gè)經(jīng)典的 Reactor 方案。

 

Reactor

單 Reactor 單進(jìn)程 / 線程

一般來(lái)說(shuō),C 語(yǔ)言實(shí)現(xiàn)的是「單 Reactor 單進(jìn)程」的方案,因?yàn)?C 語(yǔ)編寫完的程序,運(yùn)行后就是一個(gè)獨(dú)立的進(jìn)程,不需要在進(jìn)程中再創(chuàng)建線程。

而 Java 語(yǔ)言實(shí)現(xiàn)的是「單 Reactor 單線程」的方案,因?yàn)?Java 程序是跑在 Java 虛擬機(jī)這個(gè)進(jìn)程上面的,虛擬機(jī)中有很多線程,我們寫的 Java 程序只是其中的一個(gè)線程而已。

我們來(lái)看看「單 Reactor 單進(jìn)程」的方案示意圖:

可以看到進(jìn)程里有 Reactor、Acceptor、Handler 這三個(gè)對(duì)象:

Reactor 對(duì)象的作用是監(jiān)聽(tīng)和分發(fā)事件;

Acceptor 對(duì)象的作用是獲取連接;

Handler 對(duì)象的作用是處理業(yè)務(wù);

對(duì)象里的 select、accept、read、send 是系統(tǒng)調(diào)用函數(shù),dispatch 和 「業(yè)務(wù)處理」是需要完成的操作,其中 dispatch 是分發(fā)事件操作。

接下來(lái),介紹下「單 Reactor 單進(jìn)程」這個(gè)方案:

Reactor 對(duì)象通過(guò) select (IO 多路復(fù)用接口) 監(jiān)聽(tīng)事件,收到事件后通過(guò) dispatch 進(jìn)行分發(fā),具體分發(fā)給 Acceptor 對(duì)象還是 Handler 對(duì)象,還要看收到的事件類型;

如果是連接建立的事件,則交由 Acceptor 對(duì)象進(jìn)行處理,Acceptor 對(duì)象會(huì)通過(guò) accept 方法 獲取連接,并創(chuàng)建一個(gè) Handler 對(duì)象來(lái)處理后續(xù)的響應(yīng)事件;

如果不是連接建立事件, 則交由當(dāng)前連接對(duì)應(yīng)的 Handler 對(duì)象來(lái)進(jìn)行響應(yīng);

Handler 對(duì)象通過(guò) read -> 業(yè)務(wù)處理 -> send 的流程來(lái)完成完整的業(yè)務(wù)流程。

單 Reactor 單進(jìn)程的方案因?yàn)槿抗ぷ鞫荚谕粋€(gè)進(jìn)程內(nèi)完成,所以實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,不需要考慮進(jìn)程間通信,也不用擔(dān)心多進(jìn)程競(jìng)爭(zhēng)。

但是,這種方案存在 2 個(gè)缺點(diǎn):

第一個(gè)缺點(diǎn),因?yàn)橹挥幸粋€(gè)進(jìn)程,無(wú)法充分利用 多核 CPU 的性能;

第二個(gè)缺點(diǎn),Handler 對(duì)象在業(yè)務(wù)處理時(shí),整個(gè)進(jìn)程是無(wú)法處理其他連接的事件的,如果業(yè)務(wù)處理耗時(shí)比較長(zhǎng),那么就造成響應(yīng)的延遲;

所以,單 Reactor 單進(jìn)程的方案不適用計(jì)算機(jī)密集型的場(chǎng)景,只適用于業(yè)務(wù)處理非??焖俚膱?chǎng)景。

Redis 是由 C 語(yǔ)言實(shí)現(xiàn)的,它采用的正是「單 Reactor 單進(jìn)程」的方案,因?yàn)?Redis 業(yè)務(wù)處理主要是在內(nèi)存中完成,操作的速度是很快的,性能瓶頸不在 CPU 上,所以 Redis 對(duì)于命令的處理是單進(jìn)程的方案。

單 Reactor 多線程 / 多進(jìn)程

如果要克服「單 Reactor 單線程 / 進(jìn)程」方案的缺點(diǎn),那么就需要引入多線程 / 多進(jìn)程,這樣就產(chǎn)生了單 Reactor 多線程 / 多進(jìn)程的方案。

聞其名不如看其圖,先來(lái)看看「單 Reactor 多線程」方案的示意圖如下:

詳細(xì)說(shuō)一下這個(gè)方案:

Reactor 對(duì)象通過(guò) select (IO 多路復(fù)用接口) 監(jiān)聽(tīng)事件,收到事件后通過(guò) dispatch 進(jìn)行分發(fā),具體分發(fā)給 Acceptor 對(duì)象還是 Handler 對(duì)象,還要看收到的事件類型;

如果是連接建立的事件,則交由 Acceptor 對(duì)象進(jìn)行處理,Acceptor 對(duì)象會(huì)通過(guò) accept 方法 獲取連接,并創(chuàng)建一個(gè) Handler 對(duì)象來(lái)處理后續(xù)的響應(yīng)事件;

如果不是連接建立事件, 則交由當(dāng)前連接對(duì)應(yīng)的 Handler 對(duì)象來(lái)進(jìn)行響應(yīng);

上面的三個(gè)步驟和單 Reactor 單線程方案是一樣的,接下來(lái)的步驟就開(kāi)始不一樣了:

Handler 對(duì)象不再負(fù)責(zé)業(yè)務(wù)處理,只負(fù)責(zé)數(shù)據(jù)的接收和發(fā)送,Handler 對(duì)象通過(guò) read 讀取到數(shù)據(jù)后,會(huì)將數(shù)據(jù)發(fā)給子線程里的 Processor 對(duì)象進(jìn)行業(yè)務(wù)處理;

子線程里的 Processor 對(duì)象就進(jìn)行業(yè)務(wù)處理,處理完后,將結(jié)果發(fā)給主線程中的 Handler 對(duì)象,接著由 Handler 通過(guò) send 方法將響應(yīng)結(jié)果發(fā)送給 client;

單 Reator 多線程的方案優(yōu)勢(shì)在于能夠充分利用多核 CPU 的能,那既然引入多線程,那么自然就帶來(lái)了多線程競(jìng)爭(zhēng)資源的問(wèn)題。

例如,子線程完成業(yè)務(wù)處理后,要把結(jié)果傳遞給主線程的 Reactor 進(jìn)行發(fā)送,這里涉及共享數(shù)據(jù)的競(jìng)爭(zhēng)。

要避免多線程由于競(jìng)爭(zhēng)共享資源而導(dǎo)致數(shù)據(jù)錯(cuò)亂的問(wèn)題,就需要在操作共享資源前加上互斥鎖,以保證任意時(shí)間里只有一個(gè)線程在操作共享資源,待該線程操作完釋放互斥鎖后,其他線程才有機(jī)會(huì)操作共享數(shù)據(jù)。

聊完單 Reactor 多線程的方案,接著來(lái)看看單 Reactor 多進(jìn)程的方案。

事實(shí)上,單 Reactor 多進(jìn)程相比單 Reactor 多線程實(shí)現(xiàn)起來(lái)很麻煩,主要因?yàn)橐紤]子進(jìn)程 <-> 父進(jìn)程的雙向通信,并且父進(jìn)程還得知道子進(jìn)程要將數(shù)據(jù)發(fā)送給哪個(gè)客戶端。

而多線程間可以共享數(shù)據(jù),雖然要額外考慮并發(fā)問(wèn)題,但是這遠(yuǎn)比進(jìn)程間通信的復(fù)雜度低得多,因此實(shí)際應(yīng)用中也看不到單 Reactor 多進(jìn)程的模式。

另外,「單 Reactor」的模式還有個(gè)問(wèn)題,因?yàn)橐粋€(gè) Reactor 對(duì)象承擔(dān)所有事件的監(jiān)聽(tīng)和響應(yīng),而且只在主線程中運(yùn)行,在面對(duì)瞬間高并發(fā)的場(chǎng)景時(shí),容易成為性能的瓶頸的地方。

多 Reactor 多進(jìn)程 / 線程

要解決「單 Reactor」的問(wèn)題,就是將「單 Reactor」實(shí)現(xiàn)成「多 Reactor」,這樣就產(chǎn)生了第 多 Reactor 多進(jìn)程 / 線程的方案。

老規(guī)矩,聞其名不如看其圖。多 Reactor 多進(jìn)程 / 線程方案的示意圖如下(以線程為例):

方案詳細(xì)說(shuō)明如下:

主線程中的 MainReactor 對(duì)象通過(guò) select 監(jiān)控連接建立事件,收到事件后通過(guò) Acceptor 對(duì)象中的 accept  獲取連接,將新的連接分配給某個(gè)子線程;

子線程中的 SubReactor 對(duì)象將 MainReactor 對(duì)象分配的連接加入 select 繼續(xù)進(jìn)行監(jiān)聽(tīng),并創(chuàng)建一個(gè) Handler 用于處理連接的響應(yīng)事件。

如果有新的事件發(fā)生時(shí),SubReactor 對(duì)象會(huì)調(diào)用當(dāng)前連接對(duì)應(yīng)的 Handler 對(duì)象來(lái)進(jìn)行響應(yīng)。

Handler 對(duì)象通過(guò) read -> 業(yè)務(wù)處理 -> send 的流程來(lái)完成完整的業(yè)務(wù)流程。

多 Reactor 多線程的方案雖然看起來(lái)復(fù)雜的,但是實(shí)際實(shí)現(xiàn)時(shí)比單 Reactor 多線程的方案要簡(jiǎn)單的多,原因如下:

主線程和子線程分工明確,主線程只負(fù)責(zé)接收新連接,子線程負(fù)責(zé)完成后續(xù)的業(yè)務(wù)處理。

主線程和子線程的交互很簡(jiǎn)單,主線程只需要把新連接傳給子線程,子線程無(wú)須返回?cái)?shù)據(jù),直接就可以在子線程將處理結(jié)果發(fā)送給客戶端。

大名鼎鼎的兩個(gè)開(kāi)源軟件 Netty 和 Memcache 都采用了「多 Reactor 多線程」的方案。

采用了「多 Reactor 多進(jìn)程」方案的開(kāi)源軟件是 Nginx,不過(guò)方案與標(biāo)準(zhǔn)的多 Reactor 多進(jìn)程有些差異。

具體差異表現(xiàn)在主進(jìn)程中僅僅用來(lái)初始化 socket,并沒(méi)有創(chuàng)建 mainReactor 來(lái) accept 連接,而是由子進(jìn)程的 Reactor 來(lái) accept 連接,通過(guò)鎖來(lái)控制一次只有一個(gè)子進(jìn)程進(jìn)行 accept(防止出現(xiàn)驚群現(xiàn)象),子進(jìn)程 accept 新連接后就放到自己的 Reactor 進(jìn)行處理,不會(huì)再分配給其他子進(jìn)程。

 

Proactor

前面提到的 Reactor 是非阻塞同步網(wǎng)絡(luò)模式,而 Proactor 是異步網(wǎng)絡(luò)模式。

這里先給大家復(fù)習(xí)下阻塞、非阻塞、同步、異步 I/O 的概念。

先來(lái)看看阻塞 I/O,當(dāng)用戶程序執(zhí)行 read ,線程會(huì)被阻塞,一直等到內(nèi)核數(shù)據(jù)準(zhǔn)備好,并把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序的緩沖區(qū)中,當(dāng)拷貝過(guò)程完成,read 才會(huì)返回。

注意,阻塞等待的是「內(nèi)核數(shù)據(jù)準(zhǔn)備好」和「數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)」這兩個(gè)過(guò)程。過(guò)程如下圖:

阻塞 I/O

知道了阻塞 I/O ,來(lái)看看非阻塞 I/O,非阻塞的 read 請(qǐng)求在數(shù)據(jù)未準(zhǔn)備好的情況下立即返回,可以繼續(xù)往下執(zhí)行,此時(shí)應(yīng)用程序不斷輪詢內(nèi)核,直到數(shù)據(jù)準(zhǔn)備好,內(nèi)核將數(shù)據(jù)拷貝到應(yīng)用程序緩沖區(qū),read 調(diào)用才可以獲取到結(jié)果。過(guò)程如下圖:

非阻塞 I/O

注意,這里最后一次 read 調(diào)用,獲取數(shù)據(jù)的過(guò)程,是一個(gè)同步的過(guò)程,是需要等待的過(guò)程。這里的同步指的是內(nèi)核態(tài)的數(shù)據(jù)拷貝到用戶程序的緩存區(qū)這個(gè)過(guò)程。

舉個(gè)例子,如果 socket 設(shè)置了 O_NONBLOCK 標(biāo)志,那么就表示使用的是非阻塞 I/O 的方式訪問(wèn),而不做任何設(shè)置的話,默認(rèn)是阻塞 I/O。

因此,無(wú)論 read 和 send 是阻塞 I/O,還是非阻塞 I/O 都是同步調(diào)用。因?yàn)樵?read 調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間的過(guò)程都是需要等待的,也就是說(shuō)這個(gè)過(guò)程是同步的,如果內(nèi)核實(shí)現(xiàn)的拷貝效率不高,read 調(diào)用就會(huì)在這個(gè)同步過(guò)程中等待比較長(zhǎng)的時(shí)間。

而真正的異步 I/O 是「內(nèi)核數(shù)據(jù)準(zhǔn)備好」和「數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)」這兩個(gè)過(guò)程都不用等待。

當(dāng)我們發(fā)起 aio_read (異步 I/O) 之后,就立即返回,內(nèi)核自動(dòng)將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,這個(gè)拷貝過(guò)程同樣是異步的,內(nèi)核自動(dòng)完成的,和前面的同步操作不一樣,應(yīng)用程序并不需要主動(dòng)發(fā)起拷貝動(dòng)作。過(guò)程如下圖:

異步 I/O

舉個(gè)你去飯?zhí)贸燥埖睦?,你好比?yīng)用程序,飯?zhí)煤帽炔僮飨到y(tǒng)。

阻塞 I/O 好比,你去飯?zhí)贸燥?,但是飯?zhí)玫牟诉€沒(méi)做好,然后你就一直在那里等啊等,等了好長(zhǎng)一段時(shí)間終于等到飯?zhí)冒⒁贪巡硕肆顺鰜?lái)(數(shù)據(jù)準(zhǔn)備的過(guò)程),但是你還得繼續(xù)等阿姨把菜(內(nèi)核空間)打到你的飯盒里(用戶空間),經(jīng)歷完這兩個(gè)過(guò)程,你才可以離開(kāi)。

非阻塞 I/O 好比,你去了飯?zhí)?,?wèn)阿姨菜做好了沒(méi)有,阿姨告訴你沒(méi),你就離開(kāi)了,過(guò)幾十分鐘,你又來(lái)飯?zhí)脝?wèn)阿姨,阿姨說(shuō)做好了,于是阿姨幫你把菜打到你的飯盒里,這個(gè)過(guò)程你是得等待的。

異步 I/O 好比,你讓飯?zhí)冒⒁虒⒉俗龊貌巡舜虻斤埡欣锖螅扬埡兴偷侥忝媲?,整個(gè)過(guò)程你都不需要任何等待。

很明顯,異步 I/O 比同步 I/O 性能更好,因?yàn)楫惒?I/O 在「內(nèi)核數(shù)據(jù)準(zhǔn)備好」和「數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間」這兩個(gè)過(guò)程都不用等待。

Proactor 正是采用了異步 I/O 技術(shù),所以被稱為異步網(wǎng)絡(luò)模型。

現(xiàn)在我們?cè)賮?lái)理解 Reactor 和 Proactor 的區(qū)別,就比較清晰了。

Reactor 是非阻塞同步網(wǎng)絡(luò)模式,感知的是就緒可讀寫事件。在每次感知到有事件發(fā)生(比如可讀就緒事件)后,就需要應(yīng)用進(jìn)程主動(dòng)調(diào)用 read 方法來(lái)完成數(shù)據(jù)的讀取,也就是要應(yīng)用進(jìn)程主動(dòng)將 socket 接收緩存中的數(shù)據(jù)讀到應(yīng)用進(jìn)程內(nèi)存中,這個(gè)過(guò)程是同步的,讀取完數(shù)據(jù)后應(yīng)用進(jìn)程才能處理數(shù)據(jù)。

Proactor 是異步網(wǎng)絡(luò)模式, 感知的是已完成的讀寫事件。在發(fā)起異步讀寫請(qǐng)求時(shí),需要傳入數(shù)據(jù)緩沖區(qū)的地址(用來(lái)存放結(jié)果數(shù)據(jù))等信息,這樣系統(tǒng)內(nèi)核才可以自動(dòng)幫我們把數(shù)據(jù)的讀寫工作完成,這里的讀寫工作全程由操作系統(tǒng)來(lái)做,并不需要像 Reactor 那樣還需要應(yīng)用進(jìn)程主動(dòng)發(fā)起 read/write 來(lái)讀寫數(shù)據(jù),操作系統(tǒng)完成讀寫工作后,就會(huì)通知應(yīng)用進(jìn)程直接處理數(shù)據(jù)。

因此,Reactor 可以理解為「來(lái)了事件操作系統(tǒng)通知應(yīng)用進(jìn)程,讓應(yīng)用進(jìn)程來(lái)處理」,而 Proactor 可以理解為「來(lái)了事件操作系統(tǒng)來(lái)處理,處理完再通知應(yīng)用進(jìn)程」。這里的「事件」就是有新連接、有數(shù)據(jù)可讀、有數(shù)據(jù)可寫的這些 I/O 事件這里的「處理」包含從驅(qū)動(dòng)讀取到內(nèi)核以及從內(nèi)核讀取到用戶空間。

舉個(gè)實(shí)際生活中的例子,Reactor 模式就是快遞員在樓下,給你打電話告訴你快遞到你家小區(qū)了,你需要自己下樓來(lái)拿快遞。而在 Proactor 模式下,快遞員直接將快遞送到你家門口,然后通知你。

無(wú)論是 Reactor,還是 Proactor,都是一種基于「事件分發(fā)」的網(wǎng)絡(luò)編程模式,區(qū)別在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式則是基于「已完成」的 I/O 事件。

接下來(lái),一起看看 Proactor 模式的示意圖:

介紹一下 Proactor 模式的工作流程:

Proactor Initiator 負(fù)責(zé)創(chuàng)建 Proactor 和 Handler 對(duì)象,并將 Proactor 和 Handler 都通過(guò)
Asynchronous Operation Processor 注冊(cè)到內(nèi)核;

Asynchronous Operation Processor 負(fù)責(zé)處理注冊(cè)請(qǐng)求,并處理 I/O 操作;

Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor;

Proactor 根據(jù)不同的事件類型回調(diào)不同的 Handler 進(jìn)行業(yè)務(wù)處理;

Handler 完成業(yè)務(wù)處理;

可惜的是,在 Linux 下的異步 I/O 是不完善的,aio 系列函數(shù)是由 POSIX 定義的異步操作接口,不是真正的操作系統(tǒng)級(jí)別支持的,而是在用戶空間模擬出來(lái)的異步,并且僅僅支持基于本地文件的 aio 異步操作,網(wǎng)絡(luò)編程中的 socket 是不支持的,這也使得基于 Linux 的高性能網(wǎng)絡(luò)程序都是使用 Reactor 方案。

而 Windows 里實(shí)現(xiàn)了一套完整的支持 socket 的異步編程接口,這套接口就是 IOCP,是由操作系統(tǒng)級(jí)別實(shí)現(xiàn)的異步 I/O,真正意義上異步 I/O,因此在 Windows 里實(shí)現(xiàn)高性能網(wǎng)絡(luò)程序可以使用效率更高的 Proactor 方案。

 

總結(jié)

常見(jiàn)的 Reactor 實(shí)現(xiàn)方案有三種。

第一種方案單 Reactor 單進(jìn)程 / 線程,不用考慮進(jìn)程間通信以及數(shù)據(jù)同步的問(wèn)題,因此實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,這種方案的缺陷在于無(wú)法充分利用多核 CPU,而且處理業(yè)務(wù)邏輯的時(shí)間不能太長(zhǎng),否則會(huì)延遲響應(yīng),所以不適用于計(jì)算機(jī)密集型的場(chǎng)景,適用于業(yè)務(wù)處理快速的場(chǎng)景,比如 Redis 采用的是單 Reactor 單進(jìn)程的方案。

第二種方案單 Reactor 多線程,通過(guò)多線程的方式解決了方案一的缺陷,但它離高并發(fā)還差一點(diǎn)距離,差在只有一個(gè) Reactor 對(duì)象來(lái)承擔(dān)所有事件的監(jiān)聽(tīng)和響應(yīng),而且只在主線程中運(yùn)行,在面對(duì)瞬間高并發(fā)的場(chǎng)景時(shí),容易成為性能的瓶頸的地方。

第三種方案多 Reactor 多進(jìn)程 / 線程,通過(guò)多個(gè) Reactor 來(lái)解決了方案二的缺陷,主 Reactor 只負(fù)責(zé)監(jiān)聽(tīng)事件,響應(yīng)事件的工作交給了從 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多線程」的方案,Nginx 則采用了類似于 「多 Reactor 多進(jìn)程」的方案。

Reactor 可以理解為「來(lái)了事件操作系統(tǒng)通知應(yīng)用進(jìn)程,讓應(yīng)用進(jìn)程來(lái)處理」,而 Proactor 可以理解為「來(lái)了事件操作系統(tǒng)來(lái)處理,處理完再通知應(yīng)用進(jìn)程」。

因此,真正的大殺器還是 Proactor,它是采用異步 I/O 實(shí)現(xiàn)的異步網(wǎng)絡(luò)模型,感知的是已完成的讀寫事件,而不需要像 Reactor 感知到事件后,還需要調(diào)用 read 來(lái)從內(nèi)核中獲取數(shù)據(jù)。

不過(guò),無(wú)論是 Reactor,還是 Proactor,都是一種基于「事件分發(fā)」的網(wǎng)絡(luò)編程模式,區(qū)別在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式則是基于「已完成」的 I/O 事件。

參考資料

https://cloud.tencent.com/developer/article/1373468

https://blog.csdn.net/qq_27788177/article/details/98108466

https://time.geekbang.org/column/article/8805

https://www.cnblogs.com/crazymakercircle/p/9833847.html

相關(guān)推薦

電子產(chǎn)業(yè)圖譜