4 gen_event 行为
本节应与 STDLIB 中的 gen_event(3) 手册页一起阅读,该手册页详细描述了所有接口函数和回调函数。
4.1 事件处理原则
在 OTP 中,事件管理器是一个命名的对象,可以向其发送事件。事件可以是错误、警报或要记录的某些信息等。
在事件管理器中,可以安装零个、一个或多个事件处理程序。当事件管理器收到事件通知时,该事件将由所有已安装的事件处理程序处理。例如,用于处理错误的事件管理器默认情况下会安装一个处理程序,该处理程序会将错误消息写入终端。如果需要将特定时期内的错误消息保存到文件中,用户可以添加另一个处理程序来完成此操作。当不再需要将消息写入文件时,该事件处理程序会被删除。
事件管理器实现为一个进程,每个事件处理程序实现为一个回调模块。
事件管理器本质上维护着一个 {Module, State} 对列表,其中每个 Module 都是一个事件处理程序,而 State 是该事件处理程序的内部状态。
4.2 示例
用于将错误消息写入终端的事件处理程序的回调模块可能如下所示
-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).
代码将在下一节中解释。
4.3 启动事件管理器
要启动一个用于处理错误的事件管理器(如前例中所述),请调用以下函数
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 用于启动独立的事件管理器,即不是监管树的一部分的事件管理器。
4.4 添加事件处理程序
以下示例展示了如何使用 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}.
4.5 通知事件
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.6 删除事件处理程序
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).
4.7 停止
当事件管理器停止时,它会通过调用 terminate/2 来让所有已安装的事件处理程序有机会清理,与删除处理程序的方式相同。
在监管树中
如果事件管理器是监管树的一部分,则不需要停止函数。事件管理器会由其监管程序自动终止。具体操作方式由监管程序中设置的 关闭策略 定义。
独立的事件管理器
也可以通过调用以下函数来停止事件管理器
> gen_event:stop(error_man).
ok
4.8 处理其他消息
如果 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}