跳轉到

Network IO Model

從 TCP 發送數據的流程說起

要深入的理解各種 IO 模型,那麼必須先了解下產生各種 IO 的原因是什麼,要知道這其中的本質問題那麼我們就必須要知道一條消息是如何從一個人發送到另外一個人的;

以兩個應用程序通訊為例,我們來了解一下當“A”向"B" 發送一條消息,簡單來說會經過如下流程:

第一步:應用 A 把消息發送到 TCP 發送緩衝區。

第二步: TCP 發送緩衝區再把消息發送出去,經過網絡傳遞後,消息會發送到 B 服務器的 TCP 接收緩衝區。

第三步: B 再從 TCP 接收緩衝區去讀取屬於自己的數據。

img

根據上圖我們基本上了解消息發送要經過應用 A、應用 A 對應服務器的 TCP 發送緩衝區、經過網絡傳輸後消息發送到了應用 B 對應服務器 TCP 接收緩衝區、然後最終 B 應用讀取到消息。

如果理解了上面的消息發送流程,那麼我們下面開始進入文章的主題;


我們把視角切換到上面圖中的第三步, 也就是應用 B 從 TCP 緩衝區中讀取數據。

img

思考一個問題:

因為應用之間發送消息是間斷性的,也就是說在上圖中 TCP 緩衝區還沒有接收到屬於應用 B 該讀取的消息時,那麼此時應用 B 向 TCP 緩衝區發起讀取申請,TCP 接收緩衝區是應該馬上告訴應用 B 現在沒有你的數據,還是說讓應用 B 在這裡等著,直到有數據再把數據交給應用 B。

把這個問題應用到第一個步驟也是一樣,應用 A 在向 TCP 發送緩衝區發送數據時,如果 TCP 發送緩衝區已經滿了,那麼是告訴應用 A 現在沒空間了,還是讓應用 A 等待著,等 TCP 發送緩衝區有空間了再把應用 A 的數據拷貝到發送緩衝區。

什麼是阻塞 IO

如果上面的問題你已經思考過了,那麼其實你已經明白了什麼是阻塞 IO 了,所謂阻塞 IO 就是當應用 B 發起讀取數據申請時,在內核數據沒有準備好之前,應用 B 會一直處於等待數據狀態,直到內核把數據準備好了交給應用 B 才結束。

術語描述:在應用調用 recvfrom 讀取數據時,其系統調用直到數據包到達且被複製到應用緩衝區中或者發送錯誤時才返回,在此期間一直會等待,進程從調用到返回這段時間內都是被阻塞的稱為阻塞 IO;

流程:

1、應用進程向內核發起 recvfrom 讀取數據。

2、準備數據報(應用進程阻塞)。

3、將數據從內核複製到應用空間。

4、複製完成後,返回成功提示。

img

什麼是非阻塞 IO

我敢保證如果你已經理解了阻塞 IO,那麼必定已經知道了什麼是非阻塞 IO。按照上面的思路,所謂非阻塞 IO 就是當應用 B 發起讀取數據申請時,如果內核數據沒有準備好會即刻告訴應用 B,不會讓 B 在這裡等待。

術語:非阻塞 IO 是在應用調用 recvfrom 讀取數據時,如果該緩衝區沒有數據的話,就會直接返回一個 EWOULDBLOCK 錯誤,不會讓應用一直等待中。在沒有數據的時候會即刻返回錯誤標識,那也意味著如果應用要讀取數據就需要不斷的調用 recvfrom 請求,直到讀取到它數據要的數據為止。

流程:

1、應用進程向內核發起 recvfrom 讀取數據。

2、沒有數據報準備好,即刻返回 EWOULDBLOCK 錯誤碼。

3、應用進程向內核發起 recvfrom 讀取數據。

4、已有數據包準備好就進行一下步驟,否則還是返回錯誤碼。

5、將數據從內核複製到用戶空間。

6、完成後,返回成功提示。

img

IO 多路復用模型

如果你已經明白了非阻塞 IO 的工作模式,那麼接下來我們繼續了解 IO 復用模型的產生原因和思路。

思考一個問題:

我們還是把視角放到應用 B 從 TCP 緩衝區中讀取數據這個環節來。如果在並發的環境下,可能會 N 個人向應用 B 發送消息,這種情況下我們的應用就必須創建多個線程去讀取數據,每個線程都會自己調用 recvfrom 去讀取數據。那麼此時情況可能如下圖:

img

如上圖一樣,並發情況下服務器很可能一瞬間會收到幾十上百萬的請求,這種情況下應用 B 就需要創建幾十上百萬的線程去讀取數據,同時又因為應用線程是不知道什麼時候會有數據讀取,為了保證消息能及時讀取到,那麼這些線程自己必須不斷的向內核發送 recvfrom 請求來讀取數據;

那麼問題來了,這麼多的線程不斷調用 recvfrom 請求數據,先不說服務器能不能扛得住這麼多線程,就算扛得住那麼很明顯這種方式是不是太浪費資源了,線程是我們操作系統的寶貴資源,大量的線程用來去讀取數據了,那麼就意味著能做其它事情的線程就會少。

所以,有人就提出了一個思路,能不能提供一種方式,可以由一個線程監控多個網絡請求(我們後面將稱為 fd 文件描述符,linux 系統把所有網絡請求以一個 fd 來標識),這樣就可以只需要一個或幾個線程就可以完成數據狀態詢問的操作,當有數據準備就緒之後再分配對應的線程去讀取數據,這麼做就可以節省出大量的線程資源出來,這個就是 IO 多路復用模型的思路。

img

正如上圖,IO 復用模型的思路就是系統提供了一種函數可以同時監控多個 fd 的操作,這個函數就是我們常說到的 select、poll、epoll 函數,有了這個函數後,應用線程通過調用 select 函數就可以同時監控多個 fd,select 函數監控的 fd 中只要有任何一個數據狀態準備就緒了,select 函數就會返回可讀狀態,這時詢問線程再去通知處理數據的線程,對應線程此時再發起 recvfrom 請求去讀取數據。

術語描述: 進程通過將一個或多個 fd 傳遞給 select,阻塞在 select 操作上,select 幫我們偵測多個 fd 是否準備就緒,當有 fd 準備就緒時,select 返回數據可讀狀態,應用程序再調用 recvfrom 讀取數據。

img

總結:多路復用 IO 的基本思路就是通過 slect 或 poll、epoll 來監控多 fd ,來達到不必為每個 fd 創建一個對應的監控線程,從而減少線程資源創建的目的。

信號驅動 IO 模型

多路復用 IO 模型解決了一個線程可以監控多個 fd 的問題,但是 select 是採用輪詢的方式來監控多個 fd 的,通過不斷的輪詢 fd 的可讀狀態來知道是否有可讀的數據,而無腦的輪詢就顯得有點暴力,因為大部分情況下的輪詢都是無效的,所以有人就想,能不能不要我總是去問你是否數據準備就緒,能不能我發出請求後等你數據準備好了就通知我,所以就衍生了信號驅動 IO 模型。

於是信號驅動 IO 不是用循環請求詢問的方式去監控數據就緒狀態,而是在調用 sigaction 時候建立一個 SIGIO 的信號聯繫,當內核數據準備好之後再通過 SIGIO 信號通知線程數據準備好後的可讀狀態,當線程收到可讀狀態的信號後,此時再向內核發起 recvfrom 讀取數據的請求,因為信號驅動 IO 的模型下應用線程在發出信號監控後即可返回,不會阻塞,所以這樣的方式下,一個應用線程也可以同時監控多個 fd。

類似於下圖描述:

img

術語描述: 首先開啟套接口信號驅動 IO 功能,並通過系統調用 sigaction 執行一個信號處理函數,此時請求即刻返回,當數據準備就緒時,就生成對應進程的 SIGIO 信號,通過信號回調通知應用線程調用 recvfrom 來讀取數據。

img

總結:IO 多路復用模型裡面的 select 雖然可以監控多個 fd 了,但 select 其實現的本質上還是通過不斷的輪詢 fd 來監控數據狀態, 因為大部分輪詢請求其實都是無效的,所以信號驅動 IO 意在通過這種建立信號關聯的方式,實現了發出請求後只需要等待數據就緒的通知即可,這樣就可以避免大量無效的數據狀態輪詢操作。

異步 IO

其實經過了上面兩個模型的優化,我們的效率有了很大的提升,但是我們當然不會就這樣滿足了,有沒有更好的辦法,通過觀察我們發現,不管是 IO 復用還是信號驅動,我們要讀取一個數據總是要發起兩階段的請求,第一次發送 select 請求,詢問數據狀態是否準備好,第二次發送 recevform 請求讀取數據。

思考一個問題:

也許你一開始就有一個疑問,為什麼我們明明是想讀取數據,而卻非得要先發起一個 select 詢問數據狀態的請求,然後再發起真正的讀取數據請求,能不能有一種一勞永逸的方式,我只要發送一個請求我告訴內核我要讀取數據,然後我就什麼都不管了,然後內核去幫我去完成剩下的所有事情?

當然既然你想得出來,那麼就會有人做得到,有人設計了一種方案,應用只需要向內核發送一個 read 請求,告訴內核它要讀取數據後即刻返回;內核收到請求後會建立一個信號聯繫,當數據準備就緒,內核會主動把數據從內核複製到用戶空間,等所有操作都完成之後,內核會發起一個通知告訴應用,我們稱這種一勞永逸的模式為異步 IO 模型。

img

術語描述: 應用告知內核啟動某個操作,並讓內核在整個操作完成之後,通知應用,這種模型與信號驅動模型的主要區別在於,信號驅動 IO 只是由內核通知我們合適可以開始下一個 IO 操作,而異步 IO 模型是由內核通知我們操作什麼時候完成。

img

總結:異步 IO 的優化思路是解決了應用程序需要先後發送詢問請求、發送接收數據請求兩個階段的模式,在異步 IO 的模式下,只需要向內核發送一次請求就可以完成狀態詢問和數拷貝的所有操作。

再談 IO 模型裡面的同步異步

我們通常會說到同步阻塞 IO、同步非阻塞 IO,異步 IO 幾種術語,通過上面的內容,那麼我想你現在肯定已經理解了什麼是阻塞什麼是非阻塞了,所謂阻塞就是發起讀取數據請求的時,當數據還沒準備就緒的時候,這時請求是即刻返回,還是在這裡等待數據的就緒,如果需要等待的話就是阻塞,反之如果即刻返回就是非阻塞。

我們區分了阻塞和非阻塞後再來分別下同步和異步,在 IO 模型裡面如果請求方從發起請求到數據最後完成的這一段過程中都需要自己參與,那麼這種我們就稱為同步請求;反之,如果應用發送完指令後就不再參與過程了,只需要等待最終完成結果的通知,那麼這就屬於異步。

我們再看同步阻塞、同步非阻塞,他們不同的只是發起讀取請求的時候一個請求阻塞,一個請求不阻塞,但是相同的是,他們都需要應用自己監控整個數據完成的過程。而為什麼只有異步非阻塞而沒有異步阻塞呢,因為異步模型下請求指定發送完後就即刻返回了,沒有任何後續流程了,所以它注定不會阻塞,所以也就只會有異步非阻塞模型了。

高性能網絡模式:Reactor 和 Proactor

大佬們基於面向對象的思想,對 I/O 多路復用作了一層封裝,讓使用者不用考慮底層網絡 API 的細節,只需要關注應用代碼的編寫。

Reactor 模式也叫 Dispatcher 模式,即 I/O 多路復用監聽事件,收到事件後,根據事件類型分配(Dispatch)給某個進程/ 線程

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

  • Reactor 負責監聽和分發事件,事件類型包含連接事件、讀寫事件
  • 處理資源池負責處理事件,如 read -> 業務邏輯-> send;

Reactor 模式是靈活多變的,可以應對不同的業務場景,靈活在於:

  • Reactor 的數量可以只有一個,也可以有多個;
  • 處理資源池可以是單個進程/ 線程,也可以是多個進程/線程

將上面的兩個因素排列組合一下,就可以有 4 種方案選擇:

  • 單 Reactor 單進程 / 線程
  • 單 Reactor 多進程 / 線程
  • 多 Reactor 單進程 / 線程
  • 多 Reactor 多進程 / 線程

其中,「多 Reactor 單進程 / 線程」相比「單 Reactor 單進程 / 線程」,不僅複雜而且也沒有性能優勢,因此實際中並沒有應用。

剩下的 3 個方案都是比較經典的,且都有應用在實際的項目中:

  • 單 Reactor 單進程 / 線程
  • 單 Reactor 多線程 / 進程
  • 多 Reactor 多進程 / 線程

方案具體使用進程還是線程,要看使用的編程語言以及平台有關:

  • Java 語言一般使用線程,比如 Netty
  • C 語言使用進程和線程都可以,例如 Nginx 使用的是進程,Memcache 使用的是線程

Reactor

單 Reactor 單進程 / 線程

一般來說,C 語言實現的是「單 Reactor 單進程」的方案,因為 C 語言編寫完的程序,運行後就是一個獨立的進程,不需要在進程中再創建線程。

而 Java 語言實現的是「單 Reactor 單線程」的方案,因為 Java 程序是跑在 Java 虛擬機這個進程上面的,虛擬機中有很多線程,我們寫的 Java 程序只是其中的一個線程而已。

img

可以看到進程裡有 Reactor、Acceptor、Handler 這三個對象:

  • Reactor 對象的作用是監聽和分發事件
  • Acceptor 對象的作用是獲取連接
  • Handler 對象的作用是處理業務

對象裡的 select、accept、read、send 是系統調用函數,dispatch 和「業務處理」是需要完成的操作,其中 dispatch 是分發事件操作。

接下來,介紹下「單 Reactor 單進程」這個方案:

  • Reactor 對象通過 select (IO 多路復用接口) 監聽事件,收到事件後通過 dispatch 進行分發,具體分發給 Acceptor 對像還是 Handler 對象,還要看收到的事件類型
  • 如果是連接建立的事件,則交由 Acceptor 對象進行處理,Acceptor 對象會通過 accept 方法獲取連接,並創建一個 Handler 對象來處理後續的響應事件
  • 如果不是連接建立事件, 則交由當前連接對應的 Handler 對象來進行響應
  • Handler 對象通過 read -> 業務處理-> send 的流程來完成完整的業務流程

單 Reactor 單進程的方案因為全部工作都在同一個進程內完成,所以實現起來比較簡單,不需要考慮進程間通信,也不用擔心多進程競爭。

但是,這種方案存在 2 個缺點:

  • 第一個缺點,因為只有一個進程,無法充分利用多核 CPU 的性能
  • 第二個缺點,Handler 對象在業務處理時,整個進程是無法處理其他連接的事件的,如果業務處理耗時比較長,那麼就造成響應的延遲

所以,單 Reactor 單進程的方案不適用計算機密集型的場景,只適用於業務處理非常快速的場景

Redis 是由 C 語言實現的,在 Redis 6.0 版本之前採用的正是「單 Reactor 單進程」的方案,因為 Redis 業務處理主要是在內存中完成,操作的速度是很快的,性能瓶頸不在 CPU 上,所以 Redis 對於命令的處理是單進程的方案。

單 Reactor 多進程 / 多線程

如果要克服「單 Reactor 單線程 / 進程」方案的缺點,那麼就需要引入多線程 / 多進程,這樣就產生了 單 Reactor 多線程 / 多進程 的方案。

img

詳細說一下這個方案:

  • Reactor 對象通過 select (IO 多路復用接口) 監聽事件,收到事件後通過 dispatch 進行分發,具體分發給 Acceptor 對像還是 Handler 對象,還要看收到的事件類型
  • 如果是連接建立的事件,則交由 Acceptor 對象進行處理,Acceptor 對象會通過 accept 方法獲取連接,並創建一個 Handler 對象來處理後續的響應事件
  • 如果不是連接建立事件, 則交由當前連接對應的 Handler 對象來進行響應

上面的三個步驟和單 Reactor 單線程方案是一樣的,接下來的步驟就開始不一樣了:

  • Handler 對像不再負責業務處理,只負責數據的接收和發送,Handler 對象通過 read 讀取到數據後,會將數據發給子線程裡的 Processor 對象進行業務處理
  • 子線程裡的 Processor 對象就進行業務處理,處理完後,將結果發給主線程中的 Handler 對象,接著由 Handler 通過 send 方法將響應結果發送給 client

單 Reactor 多線程的優勢在於能夠充分利用多核 CPU 的能,那既然引入多線程,那麼自然就帶來了多線程競爭資源的問題。

例如,子線程完成業務處理後,要把結果傳遞給主線程的 Handler 進行發送,這裡涉及共享數據的競爭。

要避免多線程由於競爭共享資源而導致數據錯亂的問題,就需要在操作共享資源前加上互斥鎖,以保證任意時間裡只有一個線程在操作共享資源,待該線程操作完釋放互斥鎖後,其他線程才有機會操作共享數據。

聊完單 Reactor 多線程的方案,接著來看看單 Reactor 多進程的方案。

事實上,單 Reactor 多進程相比單 Reactor 多線程實現起來很麻煩,主要因為要考慮子進程與父進程的雙向通信,並且父進程還得知道子進程要將數據發送給哪個客戶端。

而多線程間可以共享數據,雖然要額外考慮並發問題,但是這遠比進程間通信的複雜度低得多,因此實際應用中也看不到單 Reactor 多進程的模式。

另外,「單 Reactor」的模式還有個問題,因為一個 Reactor 對象承擔所有事件的監聽和響應,而且只在主線程中運行,在面對瞬間高併發的場景時,容易成為性能的瓶頸的地方

多 Reactor 多進程 / 線程

要解決「單 Reactor」的問題,就是將「單 Reactor」實現成「多 Reactor」,這樣就產生了第 多 Reactor 多進程 / 線程的方案。

img

方案詳細說明如下:

  • 主線程中的 MainReactor 對象通過 select 監控連接建立事件,收到事件後通過 Acceptor 對像中的 accept 獲取連接,將新的連接分配給某個子線程
  • 子線程中的 SubReactor 對象將 MainReactor 對象分配的連接加入 select 繼續進行監聽,並創建一個 Handler 用於處理連接的響應事件
  • 如果有新的事件發生時,SubReactor 對象會調用當前連接對應的 Handler 對象來進行響應
  • Handler 對象通過 read -> 業務處理-> send 的流程來完成完整的業務流程

多 Reactor 多線程的方案雖然看起來複雜的,但是實際實現時比單 Reactor 多線程的方案要簡單的多,原因如下:

  • 主線程和子線程分工明確,主線程只負責接收新連接,子線程負責完成後續的業務處理。
  • 主線程和子線程的交互很簡單,主線程只需要把新連接傳給子線程,子線程無須返回數據,直接就可以在子線程將處理結果發送給客戶端。

大名鼎鼎的兩個開源軟件 Netty 和 Memcache 都採用了「多 Reactor 多線程」的方案。

採用了「多 Reactor 多進程」方案的開源軟件是 Nginx,不過方案與標準的多 Reactor 多進程有些差異。

具體差異表現在主進程中僅僅用來初始化 socket,並沒有創建 mainReactor 來 accept 連接,而是由子進程的 Reactor 來 accept 連接,通過鎖來控制一次只有一個子進程進行 accept(防止出現驚群現象),子進程 accept 新連接後就放到自己的 Reactor 進行處理,不會再分配給其他子進程。

Proactor

前面提到的 Reactor 是非阻塞同步網絡模式,而 Proactor 是異步網絡模式

這裡先給大家複習下阻塞、非阻塞、同步、異步 I/O 的概念。

舉個你去飯堂吃飯的例子,你好比應用程序,飯堂好比操作系統。

阻塞 I/O 好比,你去飯堂吃飯,但是飯堂的菜還沒做好,然後你就一直在那裡等啊等,等了好長一段時間終於等到飯堂阿姨把菜端了出來(數據準備的過程),但是你還得繼續等阿姨把菜(內核空間)打到你的飯盒裡(用戶空間),經歷完這兩個過程,你才可以離開。

非阻塞 I/O 好比,你去了飯堂,問阿姨菜做好了沒有,阿姨告訴你沒,你就離開了,過幾十分鐘,你又來飯堂問阿姨,阿姨說做好了,於是阿姨幫你把菜打到你的飯盒裡,這個過程你是得等待的。

異步 I/O 好比,你讓飯堂阿姨將菜做好並把菜打到飯盒里後,把飯盒送到你面前,整個過程你都不需要任何等待。

很明顯,異步 I/O 比同步 I/O 性能更好,因為異步 I/O 在「內核數據準備好」和「數據從內核空間拷貝到用戶空間」這兩個過程都不用等待。

現在我們再來理解 Reactor 和 Proactor 的區別,就比較清晰了。

  • Reactor 是非阻塞同步網絡模式,感知的是就緒可讀寫事件。在每次感知到有事件發生(比如可讀就緒事件)後,就需要應用進程主動調用 read 方法來完成數據的讀取,也就是要應用進程主動將 socket 接收緩存中的數據讀到應用進程內存中,這個過程是同步的,讀取完數據後應用進程才能處理數據。
  • Proactor 是異步網絡模式, 感知的是已完成的讀寫事件。在發起異步讀寫請求時,需要傳入數據緩衝區的地址(用來存放結果數據)等信息,這樣系統內核才可以自動幫我們把數據的讀寫工作完成,這裡的讀寫工作全程由操作系統來做,並不需要像 Reactor 那樣還需要應用進程主動發起 read/write 來讀寫數據,操作系統完成讀寫工作後,就會通知應用進程直接處理數據。

因此,Reactor 可以理解為「來了事件,操作系統通知應用進程,讓應用進程來處理」,而 Proactor 可以理解為「來了事件,操作系統來處理,處理完再通知應用進程」。這裡的「事件」指的是有新連接、有數據可讀、有數據可寫,「處理」指的是從驅動裝置讀取到內核以及從內核讀取到用戶空間。

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

無論是 Reactor,還是 Proactor,都是一種基於「事件分發」的網絡編程模式,區別在於 Reactor 模式是基於「待完成」的 I/O 事件,而 Proactor 模式則是基於「已完成」的 I/O 事件

img

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

  • Proactor Initiator 負責創建 Proactor 和 Handler 對象,並將 Proactor 和 Handler 都通過 Asynchronous Operation Processor 註冊到內核
  • Asynchronous Operation Processor 負責處理註冊請求,並處理 I/O 操作
  • Asynchronous Operation Processor 完成 I/O 操作後通知 Proactor
  • Proactor 根據不同的事件類型回調不同的 Handler 進行業務處理
  • Handler 完成業務處理

可惜的是,在 Linux 下的異步 I/O 是不完善的, aio系列函數是由 POSIX 定義的異步操作接口,不是真正的操作系統級別支持的,而是在用戶空間模擬出來的異步,並且僅僅支持基於本地文件的 aio 異步操作,網絡編程中的 socket 是不支持的,這也使得基於 Linux 的高性能網絡程序都是使用 Reactor 方案。

而 Windows 裡實現了一套完整的支持 socket 的異步編程接口,這套接口就是IOCP,是由操作系統級別實現的異步 I/O,真正意義上異步 I/O,因此在 Windows 裡實現高性能網絡程序可以使用效率更高的 Proactor 方案。

總結

常見的 Reactor 實現方案有三種。

第一種方案單 Reactor 單進程 / 線程,不用考慮進程間通信以及數據同步的問題,因此實現起來比較簡單,這種方案的缺陷在於無法充分利用多核 CPU,而且處理業務邏輯的時間不能太長,否則會延遲響應,所以不適用於計算機密集型的場景,適用於業務處理快速的場景,比如 Redis(6.0 之前) 採用的是單 Reactor 單進程的方案。

第二種方案單 Reactor 多線程,通過多線程的方式解決了方案一的缺陷,但它離高並發還差一點距離,差在只有一個 Reactor 對象來承擔所有事件的監聽和響應,而且只在主線程中運行,在面對瞬間高並發的場景時,容易成為性能的瓶頸的地方。

第三種方案多 Reactor 多進程 / 線程,通過多個 Reactor 來解決了方案二的缺陷,主 Reactor 只負責監聽事件,響應事件的工作交給了從 Reactor,Netty 和 Memcache 都採用了「多 Reactor 多線程」的方案,Nginx 則採用了類似於「多 Reactor 多進程」的方案。

Reactor 可以理解為「來了事件操作系統通知應用進程,讓應用進程來處理」,而 Proactor 可以理解為「來了事件操作系統來處理,處理完再通知應用進程」。

因此,真正的大殺器還是 Proactor,它是採用異步 I/O 實現的異步網絡模型,感知的是已完成的讀寫事件,而不需要像 Reactor 感知到事件後,還需要調用 read 來從內核中獲取數據。

不過,無論是 Reactor,還是 Proactor,都是一種基於「事件分發」的網絡編程模式,區別在於 Reactor 模式是基於「待完成」的 I/O 事件,而 Proactor 模式則是基於「已完成」的 I/O 事件。

Reference