【Reactor模式】


目錄

 

一 IO五種模型

 

二 I/O多路複用

Reactor是一個使用了同步非阻塞的I/O多路複用機制的模式。

三 Reactor模式

1 Reactor結構

2 Reactor模式流程

3 Reactor模式的實現方式

Netty 與 Reactor模式

4 面試問題:reactor模型組成

四、事件處理模式

1. Proactor

2. Asynchronous Completion Token

3. Acceptor-Connector



一 IO五種模型

IO五種模型其中信號IO很少用,主要探討阻塞、非阻塞、同步、異步四種。

IO 操作 主要分成兩部分
數據準備,將數據加載到內核緩存
② 將內核緩存中的數據加載到用戶緩存

因此:

堵塞、非堵塞的區別是在於第一階段,即數據準備階段。無論是堵塞還是非堵塞,都是用應用主動找內核要數據,而read數據的過程是‘堵塞’的,直到數據讀取完。(排隊取錢 | 取號就走)
同步、異步的區別在於第二階段,若由請求者主動的去獲取數據,則爲同步操作,需要說明的是:read/write操作也是‘堵塞’的,直到數據讀取完。(自己去ATM取錢 | 小弟幫取,再拿給自己)
若數據的read都由kernel內核完成了(在內核read數據的過程中,應用進程依舊可以執行其他的任務),這就是異步操作

 

二 I/O多路複用

I/O多路複用是指使用一個線程來檢查多個文件描述符(Socket)的就緒狀態,比如調用select和poll函數,傳入多個文件描述符,如果有一個文件描述符就緒,則返回,否則阻塞直到超時。得到就緒狀態後進行真正的操作可以在同一個線程裏執行,也可以啓動線程執行(比如使用線程池)。

一般情況下,I/O 複用機制需要事件分發器。 事件分發器的作用,將那些讀寫事件源分發給各讀寫事件的處理者
涉及到事件分發器的兩種模式稱爲:Reactor和Proactor。 Reactor模式是基於同步I/O的,而Proactor模式是和異步I/O相關的。本文主要介紹的就是 Reactor模式相關的知識。

Reactor是一個使用了同步非阻塞的I/O多路複用機制的模式。

 

三 Reactor模式

Reactor模式(反應器模式)是一種處理一個或多個客戶端併發交付服務請求的事件設計模式。當請求抵達後,服務處理程序使用I/O多路複用策略,然後同步地派發這些請求至相關的請求處理程序。

1 Reactor結構

Reactor模式的角色構成(Reactor模式一共有5中角色構成):

  • Handle(句柄或描述符,在Windows下稱爲句柄,在Linux下稱爲描述符):本質上表示一種資源(比如說文件描述符,或是針對網絡編程中的socket描述符),是由操作系統提供的;該資源用於表示一個個的事件,事件既可以來自於外部,也可以來自於內部;外部事件比如說客戶端的連接請求,客戶端發送過來的數據等;內部事件比如說操作系統產生的定時事件等。它本質上就是一個文件描述符Handle是事件產生的發源地
  • Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統調用,用於等待事件的發生(事件可能是一個,也可能是多個)。調用方在調用它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生爲止。對於Linux來說,同步事件分離器指的就是常用的I/O多路複用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的組件就是Selector;對應的阻塞方法就是select方法。
  • Event Handler(事件處理器):本身由多個回調方法構成,這些回調方法構成了與應用相關的對於某個事件的反饋機制。在Java NIO領域中並沒有提供事件處理器機制讓我們調用或去進行回調,是由我們自己編寫代碼完成的。Netty相比於Java NIO來說,在事件處理器這個角色上進行了一個升級,它爲我們開發者提供了大量的回調方法,供我們在特定事件產生時實現相應的回調方法進行業務邏輯的處理,即,ChannelHandler。ChannelHandler中的方法對應的都是一個個事件的回調。
  • Concrete Event Handler(具體事件處理器):事件處理器的實現。它本身實現了事件處理器所提供的各種回調方法,從而實現了特定於業務的邏輯。它本質上就是我們所編寫的一個個的處理器實現。
  • Initiation Dispatcher(初始分發器):實際上就是Reactor角色。它本身定義了一些規範,這些規範用於控制事件的調度方式,同時又提供了應用進行事件處理器的註冊、刪除等設施。它本身是整個事件處理器的核心所在,Initiation Dispatcher會通過Synchronous Event Demultiplexer來等待事件的發生。一旦事件發生,Initiation Dispatcher首先會分離出每一個事件,然後調用事件處理器,最後調用相關的回調方法來處理這些事件。Netty中ChannelHandler裏的一個個回調方法都是由bossGroup或workGroup中的某個EventLoop來調用的。

2 Reactor模式流程

① 初始化Initiation Dispatcher,然後將若干個Concrete Event Handler註冊到Initiation Dispatcher中。當應用向Initiation Dispatcher註冊Concrete Event Handler時,會在註冊的同時指定感興趣的事件,即,應用會標識出該事件處理器希望Initiation Dispatcher在某些事件發生時向其發出通知,事件通過Handle來標識,而Concrete Event Handler又持有該Handle。這樣,事件 ————> Handle ————> Concrete Event Handler 就關聯起來了。
② Initiation Dispatcher 會要求每個事件處理器向其傳遞內部的Handle。該Handle向操作系統標識了事件處理器。
③ 當所有的Concrete Event Handler都註冊完畢後,應用會調用handle_events方法來啓動Initiation Dispatcher的事件循環。這是,Initiation Dispatcher會將每個註冊的Concrete Event Handler的Handle合併起來,並使用Synchronous Event Demultiplexer(同步事件分離器)同步阻塞的等待事件的發生。比如說,TCP協議層會使用select同步事件分離器操作來等待客戶端發送的數據到達連接的socket handler上。
比如,在Java中通過Selector的select()方法來實現這個同步阻塞等待事件發生的操作。在Linux操作系統下,select()的實現中 a)會將已經註冊到Initiation Dispatcher的事件調用epollCtl(epfd, opcode, fd, events)註冊到linux系統中,這裏fd表示Handle,events表示我們所感興趣的Handle的事件;b)通過調用epollWait方法同步阻塞的等待已經註冊的事件的發生。不同事件源上的事件可能同時發生,一旦有事件被觸發了,epollWait方法就會返回;c)最後通過發生的事件找到相關聯的SelectorKeyImpl對象,並設置其發生的事件爲就緒狀態,然後將SelectorKeyImpl放入selectedSet中。這樣一來我們就可以通過Selector.selectedKeys()方法得到事件就緒的SelectorKeyImpl集合了。
④ 當與某個事件源對應的Handle變爲ready狀態時(比如說,TCP socket變爲等待讀狀態時),Synchronous Event Demultiplexer就會通知Initiation Dispatcher。
⑤ Initiation Dispatcher會觸發事件處理器的回調方法,從而響應這個處於ready狀態的Handle。當事件發生時,Initiation Dispatcher會將被事件源激活的Handle作爲『key』來尋找並分發恰當的事件處理器回調方法。
⑥ Initiation Dispatcher會回調事件處理器的handle_event(type)回調方法來執行特定於應用的功能(開發者自己所編寫的功能),從而相應這個事件。所發生的事件類型可以作爲該方法參數並被該方法內部使用來執行額外的特定於服務的分離與分發。

3 Reactor模式的實現方式

  • Reactor 將I/O事件分派給對應的Handler

  • Acceptor 處理客戶端新連接,並分派請求到處理器鏈中

  • Handlers 執行非阻塞讀/寫 任務

3.1 單線程Reactor模式

該模型適用於處理器鏈中業務處理組件能快速完成的場景。不過,這種單線程模型不能充分利用多核資源,所以實際使用的不多。

流程:
① 服務器端的Reactor是一個線程對象,該線程會啓動事件循環,並使用Selector來實現IO的多路複用。註冊一個Acceptor事件處理器到Reactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣Reactor會監聽客戶端向服務器端發起的連接請求事件(ACCEPT事件)。
② 客戶端向服務器端發起一個連接請求,Reactor監聽到了該ACCEPT事件的發生並將該ACCEPT事件派發給相應的Acceptor處理器來進行處理。Acceptor處理器通過accept()方法得到與這個客戶端對應的連接(SocketChannel),然後將該連接所關注的READ事件以及對應的READ事件處理器註冊到Reactor中,這樣一來Reactor就會監聽該連接的READ事件了。或者當你需要向客戶端發送數據時,就向Reactor註冊該連接的WRITE事件和其處理器。
③ 當Reactor監聽到有讀或者寫事件發生時,將相關的事件派發給對應的處理器進行處理。比如,讀處理器會通過SocketChannel的read()方法讀取數據,此時read()操作可以直接讀取到數據,而不會堵塞與等待可讀的數據到來。
④ 每當處理完所有就緒的感興趣的I/O事件後,Reactor線程會再次執行select()阻塞等待新的事件就緒並將其分派給對應處理器進行處理。

注意,Reactor的單線程模式的單線程主要是針對於I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一個線程上完成的。

但在目前的單線程Reactor模式中,不僅I/O操作在該Reactor線程上,連非I/O的業務操作也在該線程上進行處理了,這可能會大大延遲I/O請求的響應。所以我們應該將非I/O的業務邏輯操作從Reactor線程上卸載,以此來加速Reactor線程對I/O請求的響應。

改進:使用工作者線程池

 

 

與單線程Reactor模式不同的是,添加了一個工作者線程池,並將非I/O操作從Reactor線程中移出轉交給工作者線程池來執行。這樣能夠提高Reactor線程的I/O響應,不至於因爲一些耗時的業務邏輯而延遲對後面I/O請求的處理。

相對於第一種單線程的模式來說,在處理業務邏輯,也就是獲取到IO的讀寫事件之後,交由線程池來處理,這樣可以減小主reactor的性能開銷,從而更專注的做事件分發工作了,從而提升整個應用的吞吐

使用線程池的優勢:
① 通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷燬過程產生的巨大開銷。
② 另一個額外的好處是,當請求到達時,工作線程通常已經存在,因此不會由於等待創建線程而延遲任務的執行,從而提高了響應性
③ 通過適當調整線程池的大小,可以創建足夠多的線程以便使處理器保持忙碌狀態。同時還可以防止過多線程相互競爭資源而使應用程序耗盡內存或失敗

注意,在上圖的改進的版本中,所以的I/O操作依舊由一個Reactor來完成,包括I/O的accept()、read()、write()以及connect()操作。
對於一些小容量應用場景,可以使用單線程模型。但是對於高負載、大併發或大數據量的應用場景卻不合適,主要原因如下:
① 一個NIO線程同時處理成百上千的鏈路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的讀取和發送;
② 當NIO線程負載過重之後,處理速度將變慢,這會導致大量客戶端連接超時,超時之後往往會進行重發,這更加重了NIO線程的負載,最終會導致大量消息積壓和處理超時,成爲系統的性能瓶頸;

多Reactor線程模式

第三種模型比起第二種模型,是將Reactor分成兩部分,

  1. mainReactor負責監聽server socket,用來處理新連接的建立,將建立的socketChannel指定註冊給subReactor。

  2. subReactor維護自己的selector, 基於mainReactor 註冊的socketChannel多路分離IO讀寫事件,讀寫網 絡數據,對業務處理的功能,另其扔給worker線程池來完成。

 

 

Reactor線程池中的每一Reactor線程都會有自己的Selector、線程和分發的事件循環邏輯
mainReactor可以只有一個,但subReactor一般會有多個

mainReactor線程主要負責接收客戶端的連接請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通信

流程:
① 註冊一個Acceptor事件處理器到mainReactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣mainReactor會監聽客戶端向服務器端發起的連接請求事件(ACCEPT事件)。啓動mainReactor的事件循環。
② 客戶端向服務器端發起一個連接請求,mainReactor監聽到了該ACCEPT事件並將該ACCEPT事件派發給Acceptor處理器來進行處理。Acceptor處理器通過accept()方法得到與這個客戶端對應的連接(SocketChannel),然後將這個SocketChannel傳遞給subReactor線程池。
③ subReactor線程池分配一個subReactor線程給這個SocketChannel,即,將SocketChannel關注的READ事件以及對應的READ事件處理器註冊到subReactor線程中。當然你也註冊WRITE事件以及WRITE事件處理器到subReactor線程中以完成I/O寫操作。Reactor線程池中的每一Reactor線程都會有自己的Selector、線程和分發的循環邏輯。
④ 當有I/O事件就緒時,相關的subReactor就將事件派發給響應的處理器處理。注意,這裏subReactor線程只負責完成I/O的read()操作,在讀取到數據後將業務邏輯的處理放入到線程池中完成,若完成業務邏輯後需要返回數據給客戶端,則相關的I/O的write操作還是會被提交回subReactor線程來完成。

注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依舊還是在Reactor線程(mainReactor線程 或 subReactor線程)中完成的Thread Pool(線程池)僅用來處理非I/O操作的邏輯

多Reactor線程模式將“接受客戶端的連接請求”和“與該客戶端的通信”分在了兩個Reactor線程來完成。mainReactor完成接收客戶端連接請求的操作,它不負責與客戶端的通信,而是將建立好的連接轉交給subReactor線程來完成與客戶端的通信,這樣一來就不會因爲read()數據量太大而導致後面的客戶端連接請求得不到即時處理的情況。並且多Reactor線程模式在海量的客戶端併發請求的情況下,還可以通過實現subReactor線程池來將海量的連接分發給多個subReactor線程,在多核的操作系統中這能大大提升應用的負載和吞吐量。

Netty 與 Reactor模式

Netty的線程模式就是一個實現了Reactor模式的經典模式。

  • 結構對應:
    NioEventLoop ———— Initiation Dispatcher
    Synchronous EventDemultiplexer ———— Selector
    Evnet Handler ———— ChannelHandler
    ConcreteEventHandler ———— 具體的ChannelHandler的實現

  • 模式對應:
    Netty服務端使用了“多Reactor線程模式”
    mainReactor ———— bossGroup(NioEventLoopGroup) 中的某個NioEventLoop
    subReactor ———— workerGroup(NioEventLoopGroup) 中的某個NioEventLoop
    acceptor ———— ServerBootstrapAcceptor
    ThreadPool ———— 用戶自定義線程池

  • 流程:
    ① 當服務器程序啓動時,會配置ChannelPipeline,ChannelPipeline中是一個ChannelHandler鏈,所有的事件發生時都會觸發Channelhandler中的某個方法,這個事件會在ChannelPipeline中的ChannelHandler鏈裏傳播。然後,從bossGroup事件循環池中獲取一個NioEventLoop來現實服務端程序綁定本地端口的操作,將對應的ServerSocketChannel註冊到該NioEventLoop中的Selector上,並註冊ACCEPT事件爲ServerSocketChannel所感興趣的事件。
    ② NioEventLoop事件循環啓動,此時開始監聽客戶端的連接請求。
    ③ 當有客戶端向服務器端發起連接請求時,NioEventLoop的事件循環監聽到該ACCEPT事件,Netty底層會接收這個連接,通過accept()方法得到與這個客戶端的連接(SocketChannel),然後觸發ChannelRead事件(即,ChannelHandler中的channelRead方法會得到回調),該事件會在ChannelPipeline中的ChannelHandler鏈中執行、傳播。
    ④ ServerBootstrapAcceptor的readChannel方法會該SocketChannel(客戶端的連接)註冊到workerGroup(NioEventLoopGroup) 中的某個NioEventLoop的Selector上,並註冊READ事件爲SocketChannel所感興趣的事件。啓動SocketChannel所在NioEventLoop的事件循環,接下來就可以開始客戶端和服務器端的通信了。

4 面試問題:reactor模型組成

reactor模型要求主線程只負責監聽文件描述上是否有事件發生,有的話就立即將該事件通知工作線程,除此之外,主線程不做任何其他實質性的工作,讀寫數據、接受新的連接以及處理客戶請求均在工作線程中完成。其 模型組成如下:

  • Handle(句柄或描述符,在Windows下稱爲句柄,在Linux下稱爲描述符):本質上表示一種資源(比如說文件描述符,或是針對網絡編程中的socket描述符),是由操作系統提供的;它本質上就是一個文件描述符Handle是事件產生的發源地
  • Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統調用,用於等待事件的發生(事件可能是一個,也可能是多個)。調用方在調用它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生爲止。對於Linux來說,同步事件分離器指的就是常用的I/O多路複用機制,比如說select、poll、epoll等。
  • Event Handler(事件處理器):由多個回調方法構成,這些回調方法構成了對於某個事件的反饋機制。定義事件處理方法handle_event(),以供InitiationDispatcher回調使用。
  • Concrete Event Handler(具體事件處理器):事件處理器的實現。它本身實現了事件處理器所提供的各種回調方法,從而實現了特定於業務的邏輯。它本質上就是我們所編寫的一個個的處理器實現。
  • Initiation Dispatcher(初始分發器):實際上就是Reactor角色。用於管理事件處理器,用以註冊、移除事件管理器等;另外,它還作爲Reactor模式的入口調用 同步事件分離器 的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的事件處理器處理,即回調 事件處理器 中的handle_event()方法。它本身是整個事件處理器的核心所在。
  • 工作流程:reactor會通過 同步事件分離器 等待事件的發生。一旦事件發生,reactor 首先會分離出每一個事件,然後調用事件處理器,最後調用相關的回調方法來處理這些事件。

 

四、事件處理模式

在 Douglas Schmidt 的大作《POSA2》中有關於事件處理模式的介紹,其中有四種事件處理模式:

  1. Reactor  

  2. Proactor  

  3. Asynchronous Completion Token  

  4. Acceptor-Connector  

1. Proactor

本文介紹的Reactor就是其中一種,而Proactor的整體結構和reacotor的處理方式大同小異,不同的是Proactor採用的是異步非阻塞IO的方式實現,對數據的讀寫由異步處理,無需用戶線程來處理,服務程序更專注於業務事件的處理,而非IO阻塞。

2. Asynchronous Completion Token

簡單來說,ACT就是應對應用程序異步調用服務操作,並處理相應的服務完成事件。從token這個字面意思,我們大概就能瞭解到,它是一種狀態的保持和傳遞。

比如,通常應用程序會有調用第三方服務的需求,一般是業務線程請求都到,需要第三方資源的時候,去同步的發起第三方請求,而爲了提升應用性能,需要異步的方式發起請求,但異步請求的話,等數據到達之後,此時的我方應用程序的語境以及上下文信息已經發生了變化,你沒辦法去處理。

ACT 解決的就是這個問題,採用了一個token的方式記錄異步發送前的信息,發送給接受方,接受方回覆的時候再帶上這個token,此時就能恢復業務的調用場景。

 

上圖中我們可以看到在client processing 這個階段,客戶端是可以繼續處理其他業務邏輯的,不是阻塞狀態。service 返回期間會帶上token信息。  

3. Acceptor-Connector

Acceptor-Connector是於Reactor的結合,也可以看成是一種變種,它看起來很像上面介紹的Reactor第三種實現方式,但又有本質的不同。

Acceptor-Connector模式是將網絡中對等服務的連接和初始化分開處理,使系統中的連接建立及服務一旦服務初始化後就分開解除耦合。連接器主動地建立到遠地接受器組件的連接,並初始化服務處理器來處理在連接上交換的數據。同樣地,接受器被動地等待來自遠地連接器的連接請求,在這樣的請求到達時建立連接,並初始化服務處理器來處理在連接上交換的數據。隨後已初始化的服務處理器執行應用特有的處理,並通過連接器和接受器組件建立的連接來進行通信。

這麼處理的好處是:

  1. 一般而言,用於連接建立和服務初始化的策略變動的頻度要遠小於應用服務實現和通信協議。

  2. 容易增加新類型的服務、新的服務實現和新的通信協議,而又不影響現有的連接建立和服務初始化軟件。比如採用IPX/SPX通信協議或者TCP協議。

  3. 連接角色和通信角色的去耦合,連接角色只管發起連接 vs. 接受連接。通信角色只管數據交互。

  4. 將程序員與低級網絡編程API(像socket或TLI)類型安全性的缺乏屏蔽開來。業務開發關係底層通信

參考:https://www.cnblogs.com/winner-0715/p/8733787.html

參考:https://blog.csdn.net/bingxuesiyang/article/details/89888664

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