查看源代码 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}.