4  gen_event 行为

4 gen_event 行为

本节应与 STDLIB 中的 gen_event(3) 手册页一起阅读,该手册页详细描述了所有接口函数和回调函数。

在 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})

此函数会生成一个新进程(一个事件管理器)并与之建立链接。

参数 {local, error_man} 指定名称。然后事件管理器在本地注册为 error_man

如果省略名称,则事件管理器不会注册。相反,必须使用其 pid。名称也可以指定为 {global, Name},在这种情况下,事件管理器将使用 global:register_name/2 进行注册。

如果事件管理器是监管树的一部分(即由监管程序启动),则必须使用 gen_event:start_link。还有一个函数 gen_event:start 用于启动独立的事件管理器,即不是监管树的一部分的事件管理器。

以下示例展示了如何使用 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 预计返回 {ok, State},其中 State 是事件处理程序的内部状态。

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

这里,init 不需要任何输入数据,并忽略其参数。对于 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 应该是 init 的反向操作,并执行必要的清理操作。其返回值将被忽略。

对于 terminal_logger,无需执行任何清理操作

terminate(_Args, _State) ->
    ok.

对于 file_logger,必须关闭在 init 中打开的文件描述符

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

当事件管理器停止时,它会通过调用 terminate/2 来让所有已安装的事件处理程序有机会清理,与删除处理程序的方式相同。

如果事件管理器是监管树的一部分,则不需要停止函数。事件管理器会由其监管程序自动终止。具体操作方式由监管程序中设置的 关闭策略 定义。

也可以通过调用以下函数来停止事件管理器

> gen_event:stop(error_man).
ok

如果 gen_event 需要能够接收除事件以外的其他消息,则必须实现回调函数 handle_info(Info, State) 来处理这些消息。其他消息的示例包括退出消息(如果 gen_event 与其他进程建立链接(例如,通过 add_sup_handler,而不是通过监管程序建立链接))以及捕获退出信号。

handle_info({'EXIT', Pid, Reason}, State) ->
    ..code to handle exits here..
    {ok, NewState}.

还必须实现 code_change 方法。

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