兩種高效的事件處理模式

前言

      網絡服務在處理數以萬計的客戶端鏈接時,每每出現效率低下甚至徹底癱瘓,這被 稱爲 C10K 問題。C10K問題最先提出於2003年,10多年間,隨着互聯網的迅速發展,愈來愈多的網絡服務面臨的再也不是C10K問題,而是C10M問題!html

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

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

      這是最簡單的線程模型,1個線程處理1個鏈接的所有生命週期。該模型的優勢在於:這個模型足夠簡單,它能夠實現複雜的業務場景,同時,線程個數是能夠遠大於CPU個數的。然而,線程個數又不是能夠無限增大的,爲何呢?由於線程何時執行是由操做系統內核調度算法決定的,調度算法並不會考慮某個線程可能只是爲了一個鏈接服務的,時間片到了就執行一下,哪怕這個線程一執行就會不得不繼續睡眠。這樣來回的喚醒、睡眠線程在次數很少的狀況下,是廉價的,但若是操做系統的線程總數不少時,它就是昂貴的(被放大了),由於這種技術性的調度損耗會影響到線程上執行的業務代碼的時間。舉個例子,當咱們所追求的是併發處理數十萬鏈接,當幾千個線程出現時,系統的執行效率就已經沒法知足高併發了。換言之,該模型的擴展性及其糟糕,根本沒法有效知足高併發,海量鏈接的業務場景。linux

      2. 使用線程池,一樣使用阻塞式 I/O 操做算法

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

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

      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模型有三個重要的組件:

  1. 多路複用器:由操做系統提供,在linux上通常是select, poll, epoll等系統調用。
  2. 事件分發器:將多路複用器中返回的就緒事件分到對應的處理函數中。
  3. 事件處理器:負責處理特定事件的處理函數。
      圖 2. Reactor事件處理機制
      

     具體流程以下:

     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類圖

      圖 4. 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模型

      

     

  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平臺下。

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

 

備註

 

     本文有至關分量的內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有徹底參考的文章,其後附帶的連接內容更直接、更豐富,筆者只是作了一下概括&轉述,在此一併表示感謝。

 

參考

    《Linux多線程服務器編程》

    《Linux高性能服務器編程》

    《Netty系列之Netty線程模型

    《高性能網絡編程6--reactor反應堆與定時器管理

    《Comparing Two High-Performance I/O Design Patterns

相關文章
相關標籤/搜索