兩種高效的事件處理模式(Proactor和Reactor)
典型的多線程服務器的線程模型
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模式是處理並發I/O比較常見的一種模式,中心思想就是,將所有要處理的I/O事件註冊到一個中心I/O多路復用器上,同時主線程阻塞在多路復用器上;一旦有I/O事件到來或是準備就緒(區別在於多路復用器是邊沿觸發還是水平觸發),多路復用器返回並將相應I/O事件分發到對應的處理器中。
Reactor模型有三個重要的組件:
- 多路復用器:由操作系統提供,在linux上一般是select, poll, epoll等系統調用。
- 事件分發器:將多路復用器中返回的就緒事件分到對應的處理函數中。
- 事件處理器:負責處理特定事件的處理函數。
圖 2. Reactor事件處理機制
具體流程如下:
1. 註冊讀就緒事件和相應的事件處理器;
2. 事件分離器等待事件;
3. 事件到來,激活分離器,分離器調用事件對應的處理器;
4. 事件處理器完成實際的讀操作,處理讀到的數據,註冊新的事件,然後返還控制權。
Reactor模式是編寫高性能網絡服務器的必備技術之一,它具有如下的優點:
- 響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的;
- 編程相對簡單,可以最大程度的避免復雜的多線程及同步問題,並且避免了多線程/進程的切換開銷;
- 可擴展性,可以方便的通過增加Reactor實例個數來充分利用CPU資源;
- 可復用性,reactor框架本身與具體事件處理邏輯無關,具有很高的復用性;
Reactor模型開發效率上比起直接使用IO復用要高,它通常是單線程的,設計目標是希望單線程使用一顆CPU的全部資源,但也有附帶優點,即每個事件處理中很多時候可以不考慮共享資源的互斥訪問。可是缺點也是明顯的,現在的硬件發展,已經不再遵循摩爾定律,CPU的頻率受制於材料的限制不再有大的提升,而改為是從核數的增加上提升能力,當程序需要使用多核資源時,Reactor模型就會悲劇, 為什麽呢?
如果程序業務很簡單,例如只是簡單的訪問一些提供了並發訪問的服務,就可以直接開啟多個反應堆,每個反應堆對應一顆CPU核心,這些反應堆上跑的請求互不相關,這是完全可以利用多核的。例如Nginx這樣的http靜態服務器。
如果程序比較復雜,例如一塊內存數據的處理希望由多核共同完成,這樣反應堆模型就很難做到了,需要昂貴的代價,引入許多復雜的機制。
Proactor模型
圖 3. Proactor UML類圖
圖 4. Proactor模型流程圖
具體流程如下:
- 處理器發起異步操作,並關註I/O完成事件
- 事件分離器等待操作完成事件
- 分離器等待過程中,內核並行執行實際的I/O操作,並將結果數據存入用戶自定義緩沖區,最後通知事件分離器讀操作完成
- I/O完成後,通過事件分離器呼喚處理器
- 事件處理器處理用戶自定義緩沖區中的數據
從上面的處理流程,我們可以發現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,差別是:使用異步接口可以利用系統提供的讀寫並行能力,而在模擬的情況下,這需要在用戶態實現。具體的做法只需要這樣:
- 註冊讀事件(同時再提供一段緩沖區)
- 事件分離器等待可讀事件
- 事件到來,激活分離器,分離器(立即讀數據,寫緩沖區)調用事件處理器
- 事件處理器處理數據,刪除事件(需要再用異步接口註冊)
我們知道,Boost.asio庫采用的即為Proactor模型。不過Boost.asio庫在Linux平臺采用epoll實現的Reactor來模擬Proactor,並且另外開了一個線程來完成讀寫調度。
在《Linux高性能服務器編程》一書中(PS:一本好書,推薦購買閱讀!)為我們提供一種精妙的設計思路:
圖 5. 使用同步I/O模擬Proactor模型
- 主線程往epoll內核事件表中註冊socket上的讀就緒事件。
- 主線程調用epoll_wait等待socket上有數據可讀。
- 當socket上有數據可讀時,epoll_wait通知主線程。主線程從socket循環讀取數據,直到沒有更多數據可讀,然後將讀取到的數據封裝成一個請求對象並插入請求隊列。
- 睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象並處理客戶請求,然後往epoll內核事件表中註冊socket上的寫就緒事件。
- 主線程調用epoll_wait等待socket可寫。
- 當socket可寫時,epoll_wait通知主線程。主線程往socket上寫入服務器處理客戶請求的結果。
總結
兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個模塊,這個IO操作可以進行或已經完成)。在結構上兩者也有相同點:demultiplexor負責提交IO操作(異步)、查詢設備是否可操作(同步),然後當條件滿足時,就回調註冊處理函數。
不同點在於,異步情況下(Proactor),當回調註冊的處理函數時,表示IO操作已經完成;同步情況下(Reactor),回調註冊的處理函數時,表示IO設備可以進行某個操作(can read or can write),註冊的處理函數這個時候開始提交操作。
至於兩種模式孰優孰劣的問題,筆者以為差異並不是特別大。兩種模式的設計思想均足以很好的勝任高並發,海量連接的應用要求。當然,就目前筆者有限的了解,Reactor的應用實例還是更多一些,尤其是在Linux平臺下。
筆者水平有限,疏謬之處,萬望斧正!
兩種高效的事件處理模式(Proactor和Reactor)