事件驅動及其設計模式

在GUI編程中,事件是非常常見的。比如,用戶在界面點擊了按鈕,就會發送一個“點擊”事件,而相應的會有一個處理“點擊”事件的事件處理器會來處理該事件。因此,

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

事件驅動編程

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

事件驅動編程的優勢

  • 在大部分的應用場景中,事件編程優與多線程編程。
  • 相對與多線程編程來講,事件驅動編程比較容易,複雜度低,是開發者樂於接受的。
  • 大多數的GUI框架,都是使用事件驅動編程了架構的。每一個事件會綁定一個處理器,這些事件通常是點擊按鈕,選擇菜單,等等。處理器r來實現具體的行爲邏輯。
  • 事件驅動經常使用在I/O框架中,可以很好的實現I/O複用。很多高性能的I/O框架都是使用事件驅動模型的,例如:Netty、Mina、Node.js。
  • 易於調試。時間依賴只和事件有關係,而不是內部調度。問題容易暴露。

事件驅動編程的劣勢

  • 如果處理器佔用時間較長,那會阻塞應用程序的響應。
  • 無法通過時間來維護本地狀態,因爲處理器必須返回。
  • 通常在單CPU環境下,比多線程編程要快,因爲沒有鎖的因素,沒有線程切換的損耗。CPU不是併發的,這樣的話就不適合用在一些科學計算的應用中。

事件循環器(Event Loop)的實現

事件循環器(Event Loop)是一個程序結構,用於等待和發送消息和事件。事件驅動編程的代碼核心就是事件循環器,在Linux下推薦使用epoll實現,在其它沒有epoll 的系統上可以使用kqueue/ports/poll/select實現。

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

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

Reactor

首先來回想一下普通函數調用的機制:

  • 程序調用某函數->函數執行
  • 程序等待->函數將結果
  • 控制權返回給程序->程序繼續處理

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

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

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

  • “普通函數調用機制”公司HR比較懶,不會記你的聯繫方式,那怎麼辦呢,你只能面試完後自己打電話去問結果;有沒有被錄取啊,還是被拒了;
  • “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設備。

參考引用

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