【九】高性能IO模型----Reactor與proactor

參考

Reactor與Proactor

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

兩種高效的服務器設計模型:Reactor和Proactor模型

一、Reactor簡介

      Reactor模式是處理併發I/O比較常見的一種模式,用於同步I/O

中心思想是將所有要處理的I/O事件註冊到一箇中心I/O多路複用器上,同時主線程/進程阻塞在多路複用器上;

一旦有I/O事件到來或是準備就緒(文件描述符或socket可讀、寫),多路複用器返回並將事先註冊的相應I/O事件分發到對應的處理器中。
  Reactor是一種事件驅動機制,和普通函數調用的不同之處在於:應用程序不是主動的調用某個API完成處理,

而是恰恰相反,Reactor逆置了事件處理流程,應用程序需要提供相應的接口並註冊到Reactor上,如果相應的事件發生,Reactor將主動調用應用程序註冊的接口,這些接口又稱爲“回調函數”。

      Reactor模式與Observer模式在某些方面極爲相似:當一個主體發生改變時,所有依屬體都得到通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯 。

二、爲什麼使用Reactor

網絡編程爲什麼要用反應堆?有了I/O複用,有了epoll已經可以使服務器併發幾十萬連接的同時,維持高TPS了,難道這還不夠嗎?

答案是,技術層面足夠了,但在軟件工程層面卻是不夠的。

程序使用IO複用的難點在哪裏呢?

      1個請求可能由多次IO處理完成,但相比傳統的單線程完整處理請求生命期的方法,IO複用在人的大腦思維中並不自然,因爲,程序員編程中,處理請求A的時候,假定A請求必須經過多個IO操作A1-An(兩次IO間可能間隔很長時間),每經過一次IO操作,再調用IO複用時,IO複用的調用返回裏,非常可能不再有A,而是返回了請求B。即請求A會經常被請求B打斷,處理請求B時,又被C打斷。這種思維下,編程容易出錯。

形象例子:
本部分和下部分內容來自:高性能網絡編程6--reactor反應堆與定時器管理

    傳統編程方法就好像是到了銀行營業廳裏,每個窗口前排了長隊,業務員們在窗口後一個個的解決客戶們的請求。一個業務員可以盡情思考着客戶A依次提出的問題,例如:
“我要買2萬XX理財產品。“
“看清楚了,5萬起售。”
“等等,查下我活期餘額。”
“餘額5萬。”
“那就買 5萬吧。”
業務員開始錄入信息。
”對了,XX理財產品年利率8%?”
“是預期8%,最低無利息保本。“
”早不說,拜拜,我去買餘額寶。“
業務員無表情的刪着已經錄入的信息進行事務回滾。
”下一個!“
    用了IO複用則是大師業務員開始挑戰極限,在超大營業廳裏給客戶們人手一個牌子,黑壓壓的客戶們都在大廳中,有問題時舉牌申請提問,大師目光敏銳點名指定某人提問,該客戶迅速得到大師的答覆後,要經過一段時間思考,查查自己的銀袋子,諮詢下LD,才能再次進行下一個提問,直到得到完整的滿意答覆退出大廳。例如:大師剛指導A填寫轉帳單的某一項,B又來申請兌換泰銖,給了B兌換單後,C又來辦理定轉活,然後D與F在爭搶有限的圓珠筆時出現了不和諧現象,被大師叫停業務,暫時等待。
    這就是基於事件驅動的IO複用編程比起傳統1線程1請求的方式來,有難度的設計點了,客戶們都是上帝,既不能出錯,還不能厚此薄彼。
    當沒有反應堆時,我們可能的設計方法是這樣的:大師把每個客戶的提問都記錄下來,當客戶A提問時,首先查閱A之前問過什麼做過什麼,這叫聯繫上下文,然後再根據上下文和當前提問查閱有關的銀行規章制度,有針對性的回答A,並把回答也記錄下來。當圓滿回答了A的所有問題後,刪除A的所有記錄。

在程序中:
       某一瞬間,服務器共有10萬個併發連接,此時,一次IO複用接口的調用返回了100個活躍的連接等待處理。先根據這100個連接找出其對應的對象,這並不難,epoll的返回連接數據結構裏就有這樣的指針可以用。接着,循環的處理每一個連接,找出這個對象此刻的上下文狀態,再使用read、write這樣的網絡IO獲取此次的操作內容,結合上下文狀態查詢此時應當選擇哪個業務方法處理,調用相應方法完成操作後,若請求結束,則刪除對象及其上下文。

       這樣,我們就陷入了面向過程編程方法之中了,在面向應用、快速響應爲王的移動互聯網時代,這樣做早晚得把自己玩死。我們的主程序需要關注各種不同類型的請求,在不同狀態下,對於不同的請求命令選擇不同的業務處理方法。這會導致隨着請求類型的增加,請求狀態的增加,請求命令的增加,主程序複雜度快速膨脹,導致維護越來越困難,苦逼的程序員再也不敢輕易接新需求、重構。
       反應堆是解決上述軟件工程問題的一種途徑,它也許並不優雅,開發效率上也不是最高的,但其執行效率與面向過程的使用IO複用卻幾乎是等價的,所以,無論是nginx、memcached、redis等等這些高性能組件的代名詞,都義無反顧的一頭扎進了反應堆的懷抱中。
      反應堆模式可以在軟件工程層面,將事件驅動框架分離出具體業務,將不同類型請求之間用OO的思想分離。通常,反應堆不僅使用IO複用處理網絡事件驅動,還會實現定時器來處理時間事件的驅動(請求的超時處理或者定時任務的處理),就像下面的示意圖:

這幅圖有5點意思:

(1)處理應用時基於OO思想,不同的類型的請求處理間是分離的。例如,A類型請求是用戶註冊請求,B類型請求是查詢用戶頭像,那麼當我們把用戶頭像新增多種分辨率圖片時,更改B類型請求的代碼處理邏輯時,完全不涉及A類型請求代碼的修改。

(2)應用處理請求的邏輯,與事件分發框架完全分離。什麼意思呢?即寫應用處理時,不用去管何時調用IO複用,不用去管什麼調用epoll_wait,去處理它返回的多個socket連接。應用代碼中,只關心如何讀取、發送socket上的數據,如何處理業務邏輯。事件分發框架有一個抽象的事件接口,所有的應用必須實現抽象的事件接口,通過這種抽象才把應用與框架進行分離。

(3)反應堆上提供註冊、移除事件方法,供應用代碼使用,而分發事件方法,通常是循環的調用而已,是否提供給應用代碼調用,還是由框架簡單粗暴的直接循環使用,這是框架的自由。

(4)IO多路複用也是一個抽象,它可以是具體的select,也可以是epoll,它們只必須提供採集到某一瞬間所有待監控連接中活躍的連接。

(5)定時器也是由反應堆對象使用,它必須至少提供4個方法,包括添加、刪除定時器事件,這該由應用代碼調用。最近超時時間是需要的,這會被反應堆對象使用,用於確認select或者epoll_wait執行時的阻塞超時時間,防止IO的等待影響了定時事件的處理。遍歷也是由反應堆框架使用,用於處理定時事件。

三、在Reactor模式中,有5個關鍵的參與者:

描述符(handle):由操作系統提供的資源,用於識別每一個事件,如Socket描述符、文件描述符、信號的值等。在Linux中,它用一個整數來表示。事件可以來自外部,如來自客戶端的連接請求、數據等。事件也可以來自內部,如信號、定時器事件。

同步事件多路分離器(event demultiplexer):事件的到來是隨機的、異步的,無法預知程序何時收到一個客戶連接請求或收到一個信號。所以程序要循環等待並處理事件,這就是事件循環。在事件循環中,等待事件一般使用I/O複用技術實現。在linux系統上一般是select、poll、epol_waitl等系統調用,用來等待一個或多個事件的發生。I/O框架庫一般將各種I/O複用系統調用封裝成統一的接口,稱爲事件多路分離器。調用者會被阻塞,直到分離器分離的描述符集上有事件發生。

事件處理器(event handler):I/O框架庫提供的事件處理器通常是由一個或多個模板函數組成的接口。這些模板函數描述了和應用程序相關的對某個事件的操作,用戶需要繼承它來實現自己的事件處理器,即具體事件處理器。因此,事件處理器中的回調函數一般聲明爲虛函數,以支持用戶拓展。

具體的事件處理器(concrete event handler):是事件處理器接口的實現。它實現了應用程序提供的某個服務。每個具體的事件處理器總和一個描述符相關。它使用描述符來識別事件、識別應用程序提供的服務。

Reactor 管理器(reactor):定義了一些接口,用於應用程序控制事件調度,以及應用程序註冊、刪除事件處理器和相關的描述符。它是事件處理器的調度核心。 Reactor管理器使用同步事件分離器來等待事件的發生。一旦事件發生,Reactor管理器先是分離每個事件,然後調度事件處理器,最後調用相關的模 板函數來處理這個事件。

可以看出,是Reactor管理器並不是應用程序負責等待事件、分離事件和調度事件。Reactor並沒有被具體的事件處理器調度,而是管理器調度具體的事件處理器,由事件處理器對發生的事件作出處理。應用程序要做的僅僅是實現一個具體的事件處理器,然後把它註冊到Reactor管理器中。接下來的工作由管理器來完成:如果有相應的事件發生,Reactor會主動調用具體的事件處理器,由事件處理器對發生的事件作出處理。

四、Reactor的幾種模式

1 單線程模式

 

select會一直監聽着事件,事件來了之後給dispatch分發,如果建立請求的事件則分配的acceptor,由acceptor創建一個handler來處理後續的業務,如果不是建立請求的事件則分配個之前對應的handler來處理後續業務
這個情況的優點就是簡單。。。沒有多線程共享資源爭搶導致的問題。缺點就是就單線程,浪費了多CPU,並且同一時刻只有一個handler能處理,其他的得等着。
聽起來好像沒啥用啊這樣,是的絕大部分場景不適合,但是redis就是這樣用的。因爲它處理業務夠快。所以這種適合在業務處理極快的情況下使用。

2.單Reactor多線程

當業務處理不快就上多線程咯。
這個模式和上面的區別就在於具體業務實現不由handler處理的,handler只負責read數據,將數據給業務線程,然後業務線程處理完畢之後返回結果給handler,由handler send給客戶端。
這個模式的優點就是可以充分利用CPU,適合業務處理不快的情況。缺點就是多線程之間共享資源的爭搶產生的問題,並且只有一個Reactor來監聽並響應,當請求量太大時,一個Reactor可能會成爲性能瓶頸。

3.多Reactor多線程

 

mainReactor主要用來接受連接,由連接來就給acceptor,acceptor將新的連接分配個某個subReactor,然後這個subReactor將其加入自己的監聽列表,並創建一個handler來處理這個連接。之後就都由這個subReactor來select監聽來響應這個連接的請求,然後dispatch給對應的handler來read,業務處理,send。mainReactor就不管啦。
所以這種方案就等於主Reactor分流了,只有新的連接由主Reactor接受,老的連接都分給了subReactor來響應。

五、proactor模型

 Proactor是和異步I/O相關的。

       在Reactor模式中,事件分離者等待某個事件或者可應用或個操作的狀態發生(比如文件描述符可讀寫,或者是socket可讀寫),事件分離器就把這個事件傳給事先註冊的處理器(事件處理函數或者回調函數),由後者來做實際的讀寫操作。
       在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操作(相當於請求),而實際的工作是由操作系統來完成的。發起時,需要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離者得知了這個請求,它默默等待這個請求的完成,然後轉發完成事件給相應的事件處理者或者回調。

       可以看出兩者的區別:

        Reactor是在事件發生時就通知事先註冊的事件(讀寫由處理函數完成)

        Proactor是在事件發生時進行異步I/O(讀寫由OS完成),待IO完成事件分離器才調度處理器來處理。

 舉個例子,將有助於理解Reactor與Proactor二者的差異,以讀操作爲例(類操作類似)。

在Reactor(同步)中實現讀:
 - 註冊讀就緒事件和相應的事件處理器
 - 事件分離器等待事件
 - 事件到來,激活分離器,分離器調用事件對應的處理器。
 - 事件處理器完成實際的讀操作,處理讀到的數據,註冊新的事件,然後返還控制權。

Proactor(異步)中的讀:
 - 處理器發起異步讀操作(注意:操作系統必須支持異步IO)。在這種情況下,處理器無視IO就緒事件,它關注的是完成事件。
 - 事件分離器等待操作完成事件
 - 在分離器等待過程中,操作系統利用並行的內核線程執行實際的讀操作,並將結果數據存入用戶自定義緩衝區,最後通知事件分離器讀操作完成。
 - 事件分離器呼喚處理器。
 - 事件處理器處理用戶自定義緩衝區中的數據,然後啓動一個新的異步操作,並將控制權返回事件分離器。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章