事件驅動及其設計模式

華為開發者論壇發表於2020-04-16

在GUI程式設計中,事件是非常常見的。比如,使用者在介面點選了按鈕,就會傳送一個“點選”事件,而相應的會有一個處理“點選”事件的事件處理器會來處理該事件。

因此,所謂事件驅動,簡單地說就是你點什麼按鈕(即產生什麼事件),電腦執行什麼操作(即呼叫什麼函式)。當然事件也不僅限於使用者的操作. 事件驅動的核心自然是事件。從事件角度說,事件驅動程式的基本結構是由一個事件收集器、一個事件傳送器和一個事件處理器組成。事件收集器專門負責收集所有事件,包括來自使用者的(如滑鼠、鍵盤事件等)、來自硬體的(如時鐘事件等)和來自軟體的(如作業系統、應用程式本身等)。事件傳送器負責將收集器收集到的事件分發到目標物件中。事件處理器做具體的事件響應工作,它往往要到實現階段才完全確定。對於框架的使用者來說,他們唯一能夠看到的是事件處理器。這也是他們所關心的內容。

事件驅動程式設計

事件驅動程式設計通常只是用一個執行過程,CPU之間不是併發的,在處理多工的時候,事件驅動程式設計是使用協作式處理任務,而不是多執行緒的搶佔式。事件驅動簡潔易用,只需要註冊感興趣的事件,在回撥中設計邏輯就可以了。在呼叫的過程中,事件迴圈器(Event Loop)在等待事件的發生,跟著呼叫處理器。事件處理器不是搶佔式的,處理器一般只有很短的生命週期。

事件驅動程式設計的優勢

l 在大部分的應用場景中,事件程式設計優與多執行緒程式設計。

l 相對與多執行緒程式設計來講,事件驅動程式設計比較容易,複雜度低,是開發者樂於接受的。

l 大多數的GUI框架,都是使用事件驅動程式設計了架構的。每一個事件會繫結一個處理器,這些事件通常是點選按鈕,選擇選單,等等。處理器r來實現具體的行為邏輯。

l 事件驅動經常使用在I/O框架中,可以很好的實現I/O複用。很多高效能的I/O框架都是使用事件驅動模型的,例如:Netty、Mina、Node.js。

l 易於除錯。時間依賴只和事件有關係,而不是內部排程。問題容易暴露。

事件驅動程式設計的劣勢

l 如果處理器佔用時間較長,那會阻塞應用程式的響應。

l 無法通過時間來維護本地狀態,因為處理器必須返回。

l 通常在單CPU環境下,比多執行緒程式設計要快,因為沒有鎖的因素,沒有執行緒切換的損耗。CPU不是併發的,這樣的話就不適合用在一些科學計算的應用中。

事件迴圈器(Event Loop)的實現

事件迴圈器(Event Loop)是一個程式結構,用於等待和傳送訊息和事件。事件驅動程式設計的程式碼核心就是事件迴圈器,在Linux下推薦使用epoll實現,在其它沒有epoll 的系統上可以使用kqueue/ports/poll/select實現。

下圖是事件迴圈器的工作示例圖。事件迴圈器不斷接受來自客戶端(Client)的請求,事件迴圈器把請求轉交給註冊了某類事件的工作執行緒(Worker)處理。

 

根據實現的方式不同,在網路程式設計中基於事件驅動主要有兩種設計模式:Reactor和Proactor。

Reactor

首先來回想一下普通函式呼叫的機制:

l  程式呼叫某函式->函式執行

l  程式等待->函式將結果

l  控制權返回給程式->程式繼續處理

和普通函式呼叫的不同之處在於:應用程式不是主動的呼叫某個API完成處理,而是恰恰相反,應用程式需要提供相應的介面並註冊到Reactor上,如果相應的事件發生,Reactor將主動呼叫應用程式註冊的介面,這些介面又稱為“回撥函式”。

用“好萊塢原則”來形容Reactor再合適不過了:不要打電話給我們,我們會打電話通知你。

舉個例子:你去應聘某xx公司,面試結束後。

l  “普通函式呼叫機制”公司HR比較懶,不會記你的聯絡方式,那怎麼辦呢,你只能面試完後自己打電話去問結果;有沒有被錄取啊,還是被拒了;

l  “Reactor”公司HR就記下了你的聯絡方式,結果出來後會主動打電話通知你:有沒有被錄取啊,還是被拒了;你不用自己打電話去問結果,事實上也不能,因為你沒有HR的聯絡方式。

Reactor模式的優點

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

1)響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的;

2)程式設計相對簡單,可以最大程度的避免複雜的多執行緒及同步問題,並且避免了多執行緒/程式的切換開銷;

3)可擴充套件性,可以方便的通過增加Reactor例項個數來充分利用CPU資源;

4)可複用性,Reactor框架本身與具體事件處理邏輯無關,具有很高的複用性;

Reactor模式框架

使用Reactor模型,必備的幾個元件:事件源、Reactor框架、事件多路複用機制和事件處理程式,先來看看Reactor模型的整體框架,接下來再對每個元件做逐一說明。

1)事件源:Linux上是檔案描述符,Windows上就是Socket或者Handle了,這裡統一稱為“控制程式碼集”;程式在指定的控制程式碼上註冊關心的事件,比如I/O事件。

2)事件多路複用機制:由作業系統提供的I/O多路複用機制,比如select和epoll。程式首先將其關心的控制程式碼(事件源)及其事件註冊到多路複用機制上。當有事件到達時,事件多路複用機制會發出通知“在已經註冊的控制程式碼集中,一個或多個控制程式碼的事件已經就緒”。程式收到通知後,就可以在非阻塞的情況下對事件進行處理了。

3) Reactor。是事件管理的介面,內部使用事件多路複用機制註冊、登出事件;並執行事件迴圈,當有事件進入“就緒”狀態時,呼叫註冊事件的回撥函式處理事件。

4)事件處理程式。事件處理程式提供了一組介面,每個介面對應了一種型別的事件,供Reactor在相應的事件發生時呼叫,執行相應的事件處理。通常它會繫結一個有效的控制程式碼。

使用Reactor模式後,事件控制流是什麼樣子呢?可以參見下面的序列圖。

我們分別以讀操作和寫操作為例來看看Reactor中的具體步驟:

1) 應用程式註冊讀就緒事件和相關聯的事件處理器;

2) 事件分離器等待事件的發生;

3) 當發生讀就緒事件的時候,事件分離器呼叫第一步註冊的事件處理器;

4) 事件處理器首先執行實際的讀取操作,然後根據讀取到的內容進行進一步的處理。

寫入操作類似於讀取操作,只不過第一步註冊的是寫就緒事件。

Proactor

我們來看看Proactor模式中讀取操作和寫入操作的過程:

1) 應用程式初始化一個非同步讀取操作,然後註冊相應的事件處理器,此時事件處理器不關注讀取就緒事件,而是關注讀取完成事件,這是區別於Reactor的關鍵。

2) 事件分離器等待讀取操作完成事件。

3) 在事件分離器等待讀取操作完成的時候,作業系統呼叫核心執行緒完成讀取操作(非同步I/O都是作業系統負責將資料讀寫到應用傳遞進來的緩衝區供應用程式操作),並將讀取的內容放入使用者傳遞過來的快取區中。這也是區別於Reactor的一點。

4) 事件分離器捕獲到讀取完成事件後,啟用應用程式註冊的事件處理器,事件處理器直接從快取區讀取資料,而不需要進行實際的讀取操作。

Proactor中寫入操作和讀取操作,只不過感興趣的事件是寫入完成事件。

從上面可以看出,Reactor和Proactor模式的主要區別就是真正的讀取和寫入操作是有誰來完成的,Reactor中需要應用程式自己讀取或者寫入資料,而Proactor模式中,應用程式不需要進行實際的讀寫過程,它只需要從快取區讀取或者寫入即可,作業系統會讀取快取區或者寫入快取區到真正的I/O裝置。

參考引用

l  《Netty原理解析與開發實戰》

l  《分散式系統常用技術及案例分析(第二版)》

相關文章