天天看點

Reactor和proactor模式兩種高效的事件處理模式

兩種高效的事件處理模式

前言

      網絡服務在處理數以萬計的用戶端連接配接時,往往出現效率低下甚至完全癱瘓,這被 稱為 C10K 問題。C10K問題最早提出于2003年,10多年間,随着網際網路的迅速發展,越來越多的網絡服務面臨的不再是C10K問題,而是C10M問題!

典型的多線程伺服器的線程模型

      最開始的網絡程式設計伺服器是用一個while循環來監聽新的套接字連接配接,然後用一個函數處理。這種方法無法并發,效率太低。如果目前請求沒有處理完,那後面的隻能被阻塞,伺服器的吞吐量太低。于是後來采用多線程:

      1. 每個請求建立一個線程,使用阻塞式 I/O 操作

      這是最簡單的線程模型,1個線程處理1個連接配接的全部生命周期。該模型的優點在于:這個模型足夠簡單,它可以實作複雜的業務場景,同時,線程個數是可以遠大于CPU個數的。然而,線程個數又不是可以無限增大的,為什麼呢?因為線程什麼時候執行是由作業系統核心排程算法決定的,排程算法并不會考慮某個線程可能隻是為了一個連接配接服務的,時間片到了就執行一下,哪怕這個線程剛執行一會就又不得不繼續睡眠。這樣來回的喚醒、睡眠線程在次數不多的情況下,是廉價的,但如果作業系統的線程總數很多時,它就是昂貴的(被放大了),因為這種技術性的排程損耗會影響到線程上執行的業務代碼的時間。舉個例子,當我們所追求的是并發處理數十萬連接配接,當幾千個線程出現時,系統的執行效率就已經無法滿足高并發了。換言之,該模型的擴充性及其糟糕,根本無法有效滿足高并發,海量連接配接的業務場景。

      2. 使用線程池,同樣使用阻塞式 I/O 操作

      這是針對模型1的改進,但仍未從根本上解決問題

      3. 使用非阻塞I/O + I/O複用

      4. Leader/Follower 等進階模式  

兩種高效的事件處理模式

  對高并發程式設計,目前隻有一種模型,也是本質上唯一有效的玩法。網絡連接配接上的消息處理,可以分為兩個階段:等待消息準備好、消息處理。當使用預設的阻塞套接字時(例如上面提到的1個線程捆綁處理1個連接配接),往往是把這兩個階段合而為一,這樣操作套接字的代碼所在的線程就得睡眠來等待消息準備好,這導緻了高并發下線程會頻繁的睡眠、喚醒,進而影響了CPU的使用效率。

      高并發程式設計方法當然就是把兩個階段分開處理。即,等待消息準備好的代碼段,與處理消息的代碼段是分離的。當然,這也要求套接字必須是非阻塞的,否則,處理消息的代碼段很容易導緻條件不滿足時,所線上程又進入了睡眠等待階段。那麼問題來了,等待消息準備好這個階段怎麼實作?它畢竟還是等待,這意味着線程還是要睡眠的!解決辦法就是,線程主動查詢,或者讓1個線程為所有連接配接而等待!這就是IO多路複用了。多路複用就是處理等待消息準備好這件事的,但它可以同時處理多個連接配接!它也可能“等待”,是以它也會導緻線程睡眠,然而這不要緊,因為它一對多、它可以監控所有連接配接。這樣,當我們的線程被喚醒執行時,就一定是有一些連接配接已經準備好被我們的代碼執行了。

      作為一個高性能伺服器程式通常需要考慮處理三類事件: I/O事件,定時事件及信号。本文将首先首先從整體上介紹兩種高校的事件處理模型:Reactor和Proactor。

Reactor模型

      首先來回想一下普通函數調用的機制:程式調用某函數,函數執行,程式等待,函數将結果和控制權傳回給程式,程式繼續處理。Reactor釋義“反應堆”,是一種事件驅動機制。和普通函數調用的不同之處在于:應用程式不是主動的調用某個API完成處理,而是恰恰相反,Reactor逆置了事件處理流程,應用程式需要提供相應的接口并注冊到Reactor上,如果相應的時間發生,Reactor将主動調用應用程式注冊的接口,這些接口又稱為“回調函數”。 

    圖 1. Reactor模型類圖

Reactor和proactor模式兩種高效的事件處理模式

Reactor模式是處理并發I/O比較常見的一種模式,中心思想就是,将所有要處理的I/O事件注冊到一個中心I/O多路複用器上,同時主線程阻塞在多路複用器上;一旦有I/O事件到來或是準備就緒(差別在于多路複用器是邊沿觸發還是水準觸發),多路複用器傳回并将相應I/O事件分發到對應的處理器中。

Reactor和proactor模式兩種高效的事件處理模式

      Reactor模型有三個重要的元件:

  1. 多路複用器:由作業系統提供,在linux上一般是select, poll, epoll等系統調用。
  2. 事件分發器:将多路複用器中傳回的就緒事件分到對應的處理函數中。
  3. 事件處理器:負責處理特定事件的處理函數。

      圖 2. Reactor事件處理機制

Reactor和proactor模式兩種高效的事件處理模式

     具體流程如下:

     1.  注冊讀就緒事件和相應的事件處理器; 

     2.  事件分離器等待事件; 

     3.  事件到來,激活分離器,分離器調用事件對應的處理器; 

     4.  事件處理器完成實際的讀操作,處理讀到的資料,注冊新的事件,然後返還控制權。

     Reactor模式是編寫高性能網絡伺服器的必備技術之一,它具有如下的優點:

  1. 響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的;
  2. 程式設計相對簡單,可以最大程度的避免複雜的多線程及同步問題,并且避免了多線程/程序的切換開銷;
  3. 可擴充性,可以友善的通過增加Reactor執行個體個數來充分利用CPU資源;
  4. 可複用性,reactor架構本身與具體事件處理邏輯無關,具有很高的複用性;

      Reactor模型開發效率上比起直接使用IO複用要高,它通常是單線程的,設計目标是希望單線程使用一顆CPU的全部資源,但也有附帶優點,即每個事件進行中很多時候可以不考慮共享資源的互斥通路。可是缺點也是明顯的,現在的硬體發展,已經不再遵循摩爾定律,CPU的頻率受制于材料的限制不再有大的提升,而改為是從核數的增加上提升能力,當程式需要使用多核資源時,Reactor模型就會悲劇, 為什麼呢?

     如果程式業務很簡單,例如隻是簡單的通路一些提供了并發通路的服務,就可以直接開啟多個反應堆,每個反應堆對應一顆CPU核心,這些反應堆上跑的請求互不相關,這是完全可以利用多核的。例如Nginx這樣的http靜态伺服器。

     如果程式比較複雜,例如一塊記憶體資料的處理希望由多核共同完成,這樣反應堆模型就很難做到了,需要昂貴的代價,引入許多複雜的機制。

Proactor模型

      圖 3. Proactor UML類圖

Reactor和proactor模式兩種高效的事件處理模式

      圖 4. Proactor模型流程圖

Reactor和proactor模式兩種高效的事件處理模式

      具體流程如下:

  1.  處理器發起異步操作,并關注I/O完成事件
  2.  事件分離器等待操作完成事件
  3.  分離器等待過程中,核心并行執行實際的I/O操作,并将結果資料存入使用者自定義緩沖區,最後通知事件分離器讀操作完成
  4.  I/O完成後,通過事件分離器呼喚處理器
  5. 事件處理器處理使用者自定義緩沖區中的資料

      從上面的處理流程,我們可以發現proactor模型最大的特點就是Proactor最大的特點是使用異步I/O。所有的I/O操作都交由系統提供的異步I/O接口去執行。工作線程僅僅負責業務邏輯。在Proactor中,使用者函數啟動一個異步的檔案操作。同時将這個操作注冊到多路複用器上。多路複用器并不關心檔案是否可讀或可寫而是關心這個異步讀操作是否完成。異步操作是作業系統完成,使用者程式不需要關心。多路複用器等待直到有完成通知到來。當作業系統完成了讀檔案操作——将讀到的資料複制到了使用者先前提供的緩沖區之後,通知多路複用器相關操作已完成。多路複用器再調用相應的處理程式,處理資料。

      Proactor增加了程式設計的複雜度,但給工作線程帶來了更高的效率。Proactor可以在系統态将讀寫優化,利用I/O并行能力,提供一個高性能單線程模型。在windows上,由于沒有epoll這樣的機制,是以提供了IOCP來支援高并發, 由于作業系統做了較好的優化,windows較常采用Proactor的模型利用完成端口來實作伺服器。在linux上,在2.6核心出現了aio接口,但aio實際效果并不理想,它的出現,主要是解決poll性能不佳的問題,但實際上經過測試,epoll的性能高于poll+aio,并且aio不能處理accept,是以linux主要還是以Reactor模型為主。

      在不使用作業系統提供的異步I/O接口的情況下,還可以使用Reactor來模拟Proactor,差别是:使用異步接口可以利用系統提供的讀寫并行能力,而在模拟的情況下,這需要在使用者态實作。具體的做法隻需要這樣:

  1. 注冊讀事件(同時再提供一段緩沖區)
  2. 事件分離器等待可讀事件
  3. 事件到來,激活分離器,分離器(立即讀資料,寫緩沖區)調用事件處理器
  4. 事件處理器處理資料,删除事件(需要再用異步接口注冊)     

      我們知道,Boost.asio庫采用的即為Proactor模型。不過Boost.asio庫在Linux平台采用epoll實作的Reactor來模拟Proactor,并且另外開了一個線程來完成讀寫排程。

      在《Linux高性能伺服器程式設計》一書中(PS:一本好書,推薦購買閱讀!)為我們提供一種精妙的設計思路:

        圖 5. 使用同步I/O模拟Proactor模型

Reactor和proactor模式兩種高效的事件處理模式
  1. 主線程往epoll核心事件表中注冊socket上的讀就緒事件。
  2. 主線程調用epoll_wait等待socket上有資料可讀。
  3. 當socket上有資料可讀時,epoll_wait通知主線程。主線程從socket循環讀取資料,直到沒有更多資料可讀,然後将讀取到的資料封裝成一個請求對象并插入請求隊列。
  4. 睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象并處理客戶請求,然後往epoll核心事件表中注冊socket上的寫就緒事件。
  5. 主線程調用epoll_wait等待socket可寫。
  6. 當socket可寫時,epoll_wait通知主線程。主線程往socket上寫入伺服器處理客戶請求的結果。

總結

      兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個子產品,這個IO操作可以進行或已經完成)。在結構上兩者也有相同點:demultiplexor負責送出IO操作(異步)、查詢裝置是否可操作(同步),然後當條件滿足時,就回調注冊處理函數。

      不同點在于,異步情況下(Proactor),當回調注冊的處理函數時,表示IO操作已經完成;同步情況下(Reactor),回調注冊的處理函數時,表示IO裝置可以進行某個操作(can read or can write),注冊的處理函數這個時候開始送出操作。

      至于兩種模式孰優孰劣的問題,筆者以為差異并不是特别大。兩種模式的設計思想均足以很好的勝任高并發,海量連接配接的應用要求。當然,就目前筆者有限的了解,Reactor的應用執行個體還是更多一些,尤其是在Linux平台下。

      筆者水準有限,疏謬之處,萬望斧正!

以上參考:https://www.cnblogs.com/lojunren/p/3852514.html

最後貼兩張圖加強了解,參考:https://blog.csdn.net/caiwenfeng_for_23/article/details/8458299 

Reactor和proactor模式兩種高效的事件處理模式
Reactor和proactor模式兩種高效的事件處理模式