GEN_EVENT 行为模式
阅读本文时请参阅STDLIB用户手册中的章节,其中详细解释了所有的接口函数和回调函数。
事件处理原则
在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, write]),
{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})
这里的函数调用将会启动一个新的事件管理器进程。
其参数指定了该事件管理器的名字,事件管理器启动时将会在本地节点以为名注册该进程。
如果将其名字参数忽略,这个事件管理器将不会注册,此时必须使用该事件管理器进程的PID进行通讯。事件管理器的名字也可以使用,这样该事件管理器将会调用函数注册。
当事件管理器是监督树的一部分时,必须由监督树中的监督者调用函数来启动事件管理器。这里还有另外的函数用于以独立于监督树之外的形式启动事件管理器。
添加事件处理器
下面的示例展示了在SHELL中如何启动事件管理器和如何添加事件处理器
[xshumeng@xshumengs-MacBook-Pro Temporary]$ erl
Erlang/OTP 20 [erts-9.3.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Eshell V9.3.1 (abort with ^G)
1> gen_event:start_link({local, error_man}).
{ok,<0.62.0>}
2> gen_event:add_handler(error_man, terminal_logger, []).
ok
这里函数的 调用将发送消息给已注册的事件管理器,告诉它添加事件处理器。之后事件管理器会以为参数调用事件处理器中的初始化回调函数,这里回调函数的参数即是添加事件处理器时函数调用的第三个参数。添加事件处理器函数期望着事件处理器初始化回调函数返回,State是事件处理器内部状态。
init(_Args) ->
{ok, []}.
在事件处理器中,初始化函数不需要任何输入参数,并且忽略其参数,其内部状态也没有用到。
init(File) ->
{ok, Fd} = file:open(File, [read, write]),
{ok, Fd}.
在事件处理器中,初始化函数需要文件名称做参数,其内部状态用于保存已打开的文件描述符。
通知相关事件
3> gen_event:notify(error_man, no_reply).
***Error*** no_reply
ok
这里是事件管理器的名称,是本次通知的事件。
该事件被制作成消息并发送给事件管理器。当收到该事件时,事件管理器将按照事件处理器添加时的顺序调用已安装的事件处理器中的回调函数。这次调用期望事件处理器中的回调函数返回,是被调用的事件处理器中的新的内部状态。
在中:
handle_event(ErrorMsg, State) ->
io:format("***Error*** ~p~n", [ErrorMsg]),
{ok, State}.
在中:
handle_event(ErrorMsg, Fd) ->
io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),
{ok, Fd}.
卸载事件处理器
4> gen_event:delete_handler(error_man, terminal_logger, []).
ok
这里函数
的调用将发送消息给已注册的事件管理器,通知它卸载事件处理器。此时事件管理器将调用事件处理器中的回调函数,这里回调函数中的参数是函数中的第三个参数。函数与函数刚好相反,它将会做一些必要的清理工作,并且其返回值将被忽略。
在中,没有需要清理的工作进行:
terminate(_Args, _State) ->
ok.
在中,必须关闭掉在中打开的文件描述符:
terminate(_Args, Fd) ->
file:close(Fd).
停止事件管理器
当事件管理器停止时,它将像卸载事件处理器一样,调用每个已安装的事件处理器的函数用以做一些必要的清理工作。
在监督树中
If the event manager is part of a supervision tree, no stop function is needed. The event manager is automatically terminated by its supervisor. Exactly how this is done is defined by a shutdown strategy set in the supervisor.
当事件管理器处于监督树中时,将不必使用函数。事件管理器的监督者将会自动终止它。究竟如何完成这项工作是由监督者中设置的关闭策略定义的。
在独立的事件管理器中
事件管理器同样可调用:
> gen_event:stop(error_man).
ok
处理带外消息
如果能够接收除事件之外的其他消息,回调函数必须实现用以处理它们。比如链接到其他进程(非监督者)并捕获到它的退出信号,则其他消息的示例如下:
handle_info({'EXIT', Pid, Reason}, State) ->
..code to handle exits here..
{ok, NewState}.
回调函数也必须实现
code_change(OldVsn, State, Extra) ->
..code to convert state (and more) during code change
{ok, NewState}