檢視原始碼 gen_event 行為

建議同時閱讀 STDLIB 中的 gen_event 章節。

事件處理原則

在 OTP 中,事件管理器 是一個具名物件,事件可以傳送給它。事件 可以是例如錯誤、警報或一些要記錄的資訊。

在事件管理器中,會安裝零個、一個或多個事件處理器。當事件管理器收到事件通知時,所有已安裝的事件處理器都會處理該事件。例如,用於處理錯誤的事件管理器預設會安裝一個將錯誤訊息寫入終端的處理器。如果要在某段時間內將錯誤訊息也儲存到檔案,則使用者會新增另一個執行此操作的事件處理器。當不再需要記錄到檔案時,就會刪除此事件處理器。

事件管理器實作為一個程序,而每個事件處理器則實作為一個回呼模組。

事件管理器本質上維護一個 {Module, State} 配對的列表,其中每個 Module 都是一個事件處理器,而 State 則是該事件處理器的內部狀態。

範例

將錯誤訊息寫入終端的事件處理器的回呼模組可能如下所示:

-module(terminal_logger).
-behaviour(gen_event).

-export([init/1, handle_event/2, terminate/2]).

init(_Args) ->
    {ok, []}.

handle_event(ErrorMsg, State) ->
    io:format("***Error*** ~p~n", [ErrorMsg]),
    {ok, State}.

terminate(_Args, _State) ->
    ok.

將錯誤訊息寫入檔案的事件處理器的回呼模組可能如下所示:

-module(file_logger).
-behaviour(gen_event).

-export([init/1, handle_event/2, terminate/2]).

init(File) ->
    {ok, Fd} = file:open(File, read),
    {ok, Fd}.

handle_event(ErrorMsg, Fd) ->
    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),
    {ok, Fd}.

terminate(_Args, Fd) ->
    file:close(Fd).

程式碼將在接下來的章節中說明。

啟動事件管理器

要啟動一個如前述範例所述用於處理錯誤的事件管理器,請呼叫以下函式:

gen_event:start_link({local, error_man})

gen_event:start_link/1 會產生並連結到一個新的事件管理器程序。

參數 {local, error_man} 指定事件管理器應在本機註冊的名稱。也可以將名稱指定為 {global, Name},以使用 global:register_name/2 全域註冊事件管理器。

如果省略名稱,則不會註冊事件管理器。而是必須使用其 PID。

如果事件管理器是監督樹的一部分,表示它是由監督者啟動的,則必須使用 gen_event:start_link/1。還有另一個函式 gen_event:start/1,用於啟動一個不屬於監督樹的獨立事件管理器。

新增事件處理器

以下範例說明如何啟動事件管理器,並使用 Shell 將事件處理器新增至其中:

1> gen_event:start({local, error_man}).
{ok,<0.31.0>}
2> gen_event:add_handler(error_man, terminal_logger, []).
ok

此函式會傳送訊息給註冊為 error_man 的事件管理器,告知它新增事件處理器 terminal_logger。事件管理器會呼叫回呼函式 terminal_logger:init([]),其中參數 []add_handler 的第三個參數。init/1 預期會傳回 {ok, State},其中 State 是事件處理器的內部狀態。

init(_Args) ->
    {ok, []}.

在這裡,init/1 不需要任何輸入資料,並會忽略其參數。對於 terminal_logger,不會使用內部狀態。對於 file_logger,內部狀態用於儲存開啟的檔案描述器。

init(File) ->
    {ok, Fd} = file:open(File, read),
    {ok, Fd}.

通知事件

3> gen_event:notify(error_man, no_reply).
***Error*** no_reply
ok

error_man 是事件管理器的名稱,而 no_reply 則是事件。

事件會轉換為訊息並傳送至事件管理器。當接收到事件時,事件管理器會針對每個已安裝的事件處理器,以新增它們的相同順序呼叫 handle_event(Event, State)。該函式預期會傳回一個元組 {ok,State1},其中 State1 是事件處理器狀態的新值。

terminal_logger 中:

handle_event(ErrorMsg, State) ->
    io:format("***Error*** ~p~n", [ErrorMsg]),
    {ok, State}.

file_logger 中:

handle_event(ErrorMsg, Fd) ->
    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),
    {ok, Fd}.

刪除事件處理器

4> gen_event:delete_handler(error_man, terminal_logger, []).
ok

此函式會傳送訊息給註冊為 error_man 的事件管理器,告知它刪除事件處理器 terminal_logger。事件管理器會呼叫回呼函式 terminal_logger:terminate([], State),其中參數 []delete_handler 的第三個參數。terminate/2 的作用與 init/1 相反,並執行任何必要的清除作業。會忽略其傳回值。

對於 terminal_logger,不需要清除作業:

terminate(_Args, _State) ->
    ok.

對於 file_logger,必須關閉在 init 中開啟的檔案描述器:

terminate(_Args, Fd) ->
    file:close(Fd).

停止

當事件管理器停止時,它會讓每個已安裝的事件處理器有機會透過呼叫 terminate/2 來清除,與刪除處理器的方式相同。

在監督樹中

如果事件管理器是監督樹的一部分,則不需要停止函式。事件管理器會由其監督者自動終止。具體如何執行此操作,由監督者中設定的關機策略定義。

獨立事件管理器

也可以透過呼叫以下函式來停止事件管理器:

1> gen_event:stop(error_man).
ok

處理其他訊息

如果 gen_event 程序要能夠接收事件以外的其他訊息,則必須實作回呼函式 handle_info(Info, State) 來處理這些訊息。其他訊息的範例包括,如果事件管理器連結到監督者以外的其他程序(例如透過 gen_event:add_sup_handler/3)並正在捕獲結束訊號時的結束訊息。

handle_info({'EXIT', Pid, Reason}, State) ->
    %% Code to handle exits here.
    ...
    {noreply, State1}.

最後要實作的函式是 code_change/3

code_change(OldVsn, State, Extra) ->
    %% Code to convert state (and more) during code change.
    ...
    {ok, NewState}.