9 OTP设计原则——第三部分

9.5 gen_event 行为模式

这一章节结合 stdlib man page中的 gen_event(3) 阅读,man page中详细介绍了gen_event行为模式的接口函数和回调函数。

9.5.1 事件处理原则

在OTP中,事件管理器是一个可以接收并记录事件的命名对象。错误、警告和其他信息都是可以被记录的事件。

在事件管理器中,可以安装一个或多个事件处理模块。当事件处理器接收到事件通知时,所有安装的事件处理模块会处理这个事件。例如,处理错误的事件管理器默认有一个事件处理器,它会把错误日志写到终端。如果特定时期的错误信息需要保存到文件中,那么用户可以安装一个事件处理器来做这件事。当不需要写入文件时,这个事件处理器可以被删除。

事件管理器可以用进程的形式实现,而事件管理器可以实现它的回调模块。

事件管理器内部维护一个{Module, State}列表,每个Module是一个事件处理器,State是事件处理器的内部状态。

9.5.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).

下面小节详细介绍这个例子。

9.5.3 启动事件管理器

为了启动前一节描述的处理错误的事件管理器,要调用下面的函数:

gen_event:start_link({local, error_man}).

这个函数创建并链接一个新的进程,一个事件管理器。

参数{local, error_man}表示进程名,表示在本地注册error_man;如果省略名字,gen_fsm进程没有注册,因此要用到Pid。name也可以是{global, Name}的形式,这样的话事件管理器要用global:register_name/2注册。

当gen_event进程作为监控树的一部分时,gen_event:start_link会被监控进程调用以启动进程;作为独立进程时,可以调用gen_event:start启动。

9.5.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}.
9.5.5 事件通知
3> gen_event:notify(error_man, no_reply).
***Error*** no_reply
ok

error_man是事件管理器的名称,no_reply是事件。

事件会被当做消息发送给消息管理器。当事件到达时,事件管理器按照安装顺序调用每个事件处理器的handle_event(Event, State)。handle_event函数应该返回{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}.
9.5.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,open中打开的文件描述符需要关闭:

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

当事件管理器停止时,会调用每个事件处理器的terminate/2函数做清理工作,类似删除事件处理器。

在监控树中

如果事件管理器是监控树的一部分,不需要提供专门的stop函数,会被它的监控进程supervisor自动终止。具体如何终止通过在监控进程中设置shutdown strategy实现。

独立gen_fsm进程

事件管理器可以通过调用下面的函数停止:

> gen_event:stop(error_man).
ok
9.3.8 其他消息处理

如果gen_event收到其他消息,必须实现回调函数handle_info(Info, State)用来处理他们(这里原文似乎有个错误)。其他消息包括退出信号,当gen_event进程链接其他进程并捕捉exit信号时。

handle_info({'EXIT', Pid, Reason}, StateName, StateData) ->
    ..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}

9.6 supervisor 行为模式

这一章节结合 stdlib man page中的 supervisor(3) 阅读,man page中详细介绍了supervisor行为模式的接口函数和回调函数。

9.6.1 监控原则

一个监控器负责启动、停止和监控它的所有子进程。监控器的基本思想是必要的时候通过重启保持子进程存活。

子进程的启动和监控通过 子进程规范(child specifications) 列表来确定。子进程按列表顺序启动,按相反顺序终止。

9.6.2 例子

一个gen_server行为模式服务器的supervisor的回调模块如下代码:

-module(ch_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(ch_sup, []).
init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [#{id => ch3,start => {ch3, start_link, []},restart => permanent,shutdown => brutal_kill,type => worker,modules => [cg3]}],
    {ok, {SupFlags, ChildSpecs}}.

init/1返回的SupFlags变量代表监视器标志。

返回列表的ChildSpecs变量是子进程规范的列表。

9.6.3 监视器标志

这是监视器标志位的类型定义:

sup_flags() = #{strategy => strategy(), % optional
                intensity => non_neg_integer(), % optional
                period => pos_integer()} % optional
strategy() = one_for_all | one_for_one | rest_for_one | simple_one_for_one
  • strategy 表示重启策略
  • intensityperiod 表明 重启强度(maximum restart intensity)
9.6.4 重启策略

重启策略通过回调函数init返回的监视器标志的strategy键值确定:

SupFlags = #{strategy => Stratege, ...}

strategy键是可选的,默认是 one_for_one

one_for_one

如果一个子进程意外终止,只有它自己会被重启。

one_for_all

如果一个子进程意外终止,所有子进程都会被终止,然后重启——类似进程组“同生同灭”的概念。

rest_for_one

如果一个子进程意外终止,剩余的子进程(按启动顺序排列,在终止的进程后面启动的子进程)也被终止。随后这些子进程会重启——说明这种模式下子进程的顺序是有关联的。

simple_one_for_one

具体查看 simple_one_for_one 监视器 部分。

9.6.5 重启强度(Maximum Restart Intensity)

监视器有一个内建机制用于限制给定时间段内最多可以重启的次数。监视器标志中的键值 intensityperiod 来确定。

SupFlags = #{intensity => MaxR, period => MaxT, ...}

如果在过去的MaxT秒时间超过MaxR次重启发生,那么监视器会终止所有子进程,然后自己退出。

当监视器终止时,高一级的监视器会采取动作。要么重启已终止的监视器,要么终止自己。

重启机制的目的是为了防止一个进程因为同样的情况反复重启、退出。

键值 intensityperiod 是可选的, 默认为 1 和 5。

9.6.6 子进程规范

子进程规范的类型定义如下:

child_spec() = #{
            id => child_id(),       %% 必须
            start => mfargs(),      %% 必须
            restart => restart(),   %% 可选
            shutdown => shutdown(), %% 可选
            type => worker(),       %% 可选
            modules => modules()    %% 可选
            }
child_id() = term()
mfargs() = {M :: module(), F :: atom(), A :: [term()]}
modules() = [module()] | dynamic
restart() = permanent | transient | temporary
shutdown() = brutal_kill | timeout()
worker() = worker | supervisor
  • id 用于监视器内部明确标识子进程,id键是必需的。注意这个标识有时候被称为”name”。我们尽量使用“identifier”或者“id”,但是为了向下兼容,有时候会看到“name”的使用,例如在某些错误信息中。

  • start 定义了子进程要启动的线程。是模块-函数-参数元组,用于 apply(M, F, A)。它会导致下面的几个调用:

    • supervisor:start_link
    • gen_server:start_link
    • gen_fsm: start_link
    • gen_statem: start_link
    • gen_event: start_link
    • 和上面函数兼容的其它函数。更多细节阅读 supervisor(3) 的man page。 start是必需的。
  • restart 定义了如何重启一个终止的进程。

    • permanent 子进程一定会被重启
    • temporary 子进程不会被重启(监视器的重启策略是rest_for_one或one_for_all,其他进程可能导致temporary进程终止)。
    • transient 子进程只有意外终止时会被重启,即退出原因不是normal、shutdown或{shutdown, Term}。 restart是可选的,默认值为permanent。
  • shutdown 定义子进程如何被终止。

    • brutal_kill 表示子进程会无条件的被 exit(Child, kill) 终止。
    • 整型超时值表示监视器会通过exit(Child, shutdown)通知子进程退出,然后等到退出信号。如果特定时间内没有收到信号,就会用exit(Child, kill)无条件退出。
    • 如果子进程是监视器,shutdown被设为infinity,从而让子进程树有充足的时间完成终止。也可以给工作进程设定这个值。看一下下面的提醒:

    警告:要小心给工作进程设定关闭时间为infinity。因为在这种情况下,监控树的终止取决于子进程;这个资金从必须用安全的方式实现,确保清理过程一定会返回。 shutdown标志是可选的。对于worker进程,默认是5000;对于supervisor子进程,默认值是infinity。

  • type 标志表明子进程是工作进程或监控器。 type是可选的,默认为worker进程。

  • modules 是只有一个成员的列表,[Module],如果子进程是supervisor、gen_server、gen_fsm或gen_statem,Module是回调模块的名称。如果子进程是gen_event,这个值应该是dynamic。 模块信息用于升级或回退版本时发行控制,查看 Release Handling 部分。 modules键是可选的,默认情况下为子进程启动{M, F, A}中的M。

例子 :服务器ch3的子进程标识符如下:

#{id => ch3, start => {ch3, start_link, []}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [ch3]}

或者省去默认值,简单写:

#{id => ch3, start => {ch3, start_link, []}, shutdown => brutal_kill} (官方文档似乎少了个逗号)

例子 :子进程为事件管理器:

#{id => error_man, start => {gen_event, start_link, [{local, error_man}]}, modules => dynamic}

ch3服务器和事件管理器的注册进程应该一直可以访问,因此重启策略使用默认值 permanent。

ch3退出时不需要清理资源,因此用brutal_kill终止策略。error_man需要一些时间来删除事件处理器,因此使用默认值5000ms。

例子 :子进程是监视器:

#{id => sup, start => {sup, start_link, []}, restart => transient, type => supervisor} % will cause default shutdown=>infinity
启动监视器

在前面的章节,监视器通过调用ch_sup:start_link()启动:

start_link() ->
    supervisor:start_link(ch_sup, []).

ch_sup:start_link调用 supervisor:start_link/2,创建并链接新的监视器进程。

  • 第一个参数,ch_sup,是回调模块的名称,也是init回调函数所在的模块。
  • 第二个参数,[],是传给init函数的参数,这里是空。

这个例子中,监视器没有注册,因此Pid需要用到。注册名可以通过调用 supervisor:start_link({local, Name}, Module, Args) 或者 supervisor:start_link({global, Name}, Module, Args)。

新进程回调 ch_sup:init([]),init应该返回 {ok, {SupFlags, ChildSpecs}}:

init(_Args) -> 
    SupFlags = #{}, 
    ChildSpecs = [#{id => ch3, start => {ch3, start_link, []}, shutdown => brutal_kill}], 
    {ok, {SupFlags, ChildSpecs}}.

监视器会根据子进程规范启动进程,这里会启动ch3进程。

supervisor:start_link是同步的,因此所有子进程启动之后它才会返回。

9.6.8 新增子进程

除了固定的监控树,动态子进程可以被添加到已有的监控树:

supervisor:start_child(Sup, ChildSpec)

Sup是pid或者进程名。ChildSpec是 子进程规范 .

通过调用start_child/2新增的子进程类似其他子进程,只有一个区别:监视器退出并重启后,动态添加的子进程会丢失。

9.6.9 停止子进程

任何子进程——动态添加或者静态生成的,都可以通过下面的调用终止:

supervisor:terminate_child(Sup, Id)

可以用下面的调用删除一个已停止的子进程:

supervisor:delete_child(Sup, Id)

Sup是监视器的pid或者注册名,Id是 子进程规范 中指定的id键。

类似动态添加的子进程,删除子进程之后,监视器重启后它会丢失——即不会再次生成。

9.6.10 简单一对一监视器

监视器重启策略simple_one_for_one是one_for_one的简单版本,所有子进程用同样的进程实例模板动态添加。

下面的例子是一个simple_one_for_one监视器:

-module(simple_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(simple_sup, []).
init(_Args) ->
    SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1},
    ChildSpecs = [#{id => call, start => {call, start_link, []}, shutdown => brutal_kill}],
    {ok, {SupFlags, ChildSpecs}}.

监视器启动时,并不会创建任何子进程。相反,所有子进程通过下面的调用动态添加:

supervisor:start_child(Sup, List)

Sup是监视器的pid或注册名,List是自定义的原子列表,会被添加到子进程的参数列表。如果启动函数是{ M, F, A},这个子进程会调用 apply(M, F, A++List) 启动。

例如,给simple_sup添加一个子进程:

supervisor:start_child(Pid, [id1])

这个子进程通过调用apply(call, start_link, []++[id1])启动,实际上如下:

call:start_link(id1)

simple_one_for_one监视器的子进程可以通过下面的调用终止:

supervisor:terminate_child(Sup, Pid)

Sup和Pid的意义不言而喻。

因为一个simple_one_for_one可以有很多子进程,因此会异步终止它们。这意味着子进程可以并行的清理资源,终止顺序也不一定。

9.6.11 终止。

因为监视器是监控树的一部分,它会被它的监视器自动终止。被要求关闭时,它会按照启动的相反顺序终止所有子进程,最后停止自己。

9.7 sys和proc_lib

sys模块有一些用行为模式实现的函数,可以简单调试进程。还有一些函数,结合proc_lib模块,可以用于实现遵从OTP设计原则的 特殊进程 而不必使用标准行为模式。这些函数可以用于实现自定义的行为模式。

sys和proc_lib模块都属于STDLIB应用。

9.7.1 简单调试

sys模块有一些使用行为模式实现的调试函数。gen_fsm行为模式 章节的code_lock例子用来演示:

% erl
Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]
Eshell V5.2.3.6 (abort with ^G)
1> code_lock:start_link([1,2,3,4]).
{ok,<0.32.0>}
2> sys:statistics(code_lock, true).
ok
3> sys:trace(code_lock, true).
ok
4> code_lock:button(4).
*DBG* code_lock got event {button,4} in state closed
ok
*DBG* code_lock switched to state closed
5> code_lock:button(3).
*DBG* code_lock got event {button,3} in state closed
ok
*DBG* code_lock switched to state closed
6> code_lock:button(2).
*DBG* code_lock got event {button,2} in state closed
ok
*DBG* code_lock switched to state closed
7> code_lock:button(1).
*DBG* code_lock got event {button,1} in state closed
ok
OPEN DOOR
*DBG* code_lock switched to state open
*DBG* code_lock got event timeout in state open
CLOSE DOOR
*DBG* code_lock switched to state closed
8> sys:statistics(code_lock, get).
{ok,[{start_time,{{2003,6,12},{14,11,40}}},{current_time,{{2003,6,12},{14,12,14}}},{reductions,333},{messages_in,5},{messages_out,0}]}
9> sys:statistics(code_lock, false).
ok
10> sys:trace(code_lock, false).
ok
11> sys:get_status(code_lock).
{status,<0.32.0>,
    {module,gen_fsm},
    [[{'$ancestors',[<0.30.0>]},
     {'$initial_call',{gen,init_it,[gen_fsm,<0.30.0>,<0.30.0>,{local,code_lock},code_lock,[1,2,3,4],[]]}}],
     running,<0.30.0>,[],
     [code_lock,closed,{[],[1,2,3,4]},code_lock,infinity]]}
9.7.2 特殊进程

这一节介绍符合OTP设计规范的特殊进程,而不使用标准行为模式。这样的进程可以用于: * 以某种方式启动进程从而适应一个监控树 * 支持 sys 的 调试机制 * 处理 系统消息

系统消息是由特殊含义的消息,在监控树中使用。典型的系统消息有跟踪输出、暂停和恢复进程运行(发型控制中使用)。使用标准行为模式实现的进程自动理解这些消息。

例子

来自 Overview 中的简单例子,用sys和proc_lib实现,来适应监控树:

-module(ch4).
-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1]).
-export([system_continue/3, system_terminate/4, write_debug/3, system_get_state/1, system_replace_state/2]).

start_link() ->
    proc_lib:start_link(ch4, init, [self()]).
    
alloc() ->
    ch4 ! {self(), alloc},
    receive
        {ch4, Res} ->
            Res
    end.
    
free(Ch) ->
    ch4 ! {free, Ch},
    ok.
    
init(Parent) ->
    register(ch4, self()),
    Chs = channels(),
    Deb = sys:debug_options([]),
    proc_lib:init_ack(Parent, {ok, self()}),
    loop(Chs, Parent, Deb).
    
loop(Chs, Parent, Deb) ->
    receive
        {From, alloc} ->
            Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,
            ch4, {in, alloc, From}),
            {Ch, Chs2} = alloc(Chs),
            From ! {ch4, Ch},
            Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}),
            loop(Chs2, Parent, Deb3);
        {free, Ch} ->
            Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, {free, Ch}}),
            Chs2 = free(Ch, Chs),
            loop(Chs2, Parent, Deb2);
        {system, From, Request} ->
            sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)
    end.
    
system_continue(Parent, Deb, Chs) ->
    loop(Chs, Parent, Deb).
    
system_terminate(Reason, _Parent, _Deb, _Chs) ->
    exit(Reason).
    
system_get_state(Chs) ->
    {ok, Chs}.
    
system_replace_state(StateFun, Chs) ->
    NChs = StateFun(Chs),
    {ok, NChs, NChs}.
    
write_debug(Dev, Event, Name) ->
    io:format(Dev, "~p event = ~p~n", [Name, Event]).

ch4中同样展示了如何使用sys提供的简单调试函数:

1> ch4:start_link().
{ok,<0.30.0>}
2> sys:statistics(ch4, true).
ok
3> sys:trace(ch4, true).
ok
4> ch4:alloc().
ch4 event = {in,alloc,<0.25.0>}
ch4 event = {out,{ch4,ch1},<0.25.0>}
ch1
5> ch4:free(ch1).
ch4 event = {in,{free,ch1}}
ok
6> sys:statistics(ch4, get).
{ok,[{start_time,{{2003,6,13},{9,47,5}}},{current_time,{{2003,6,13},{9,47,56}}},{reductions,109},{messages_in,2},{messages_out,1}]}
7> sys:statistics(ch4, false).
ok
8> sys:trace(ch4, false).
ok
9> sys:get_status(ch4).
{status,<0.30.0>,{module,ch4},[[{'$ancestors',[<0.25.0>]},{'$initial_call',{ch4,init,[<0.25.0>]}}],running,<0.25.0>,[],[ch1,ch2,ch3]]}
启动进程

proc_lib中的函数用于启动进程。有几个函数可以使用,例如spawn_link/3,4用于异步启动,start_link/3,4,5用于同步启动。

使用这些函数启动的进程存储关键信息(例如,关于进程祖先和初始调用)对于监控树中的进程是必要的。

如果进程因为normal或shutdown之外的原因终止,会生成crash报告。更多信息查看 SASL User’s Guide

这个例子中,用到了同步调用。进程通过ch4:start_link()启动:

start_link() ->
    proc_lib:start_link(ch4, init, [self()]).

ch4:start_link调用proc_lib:start_link函数。这个函数需要模块名、进程名和参数,启动并链接一个新进程。新进程通过执行特定函,这里是ch4:init(Pid),Pid是第一个进程,对于init来说是父进程。

初始化工作,包括注册进程名,会在init中完成。新进程必须知道它是被哪个父进程启动的:

init(Parent) ->
    ...
    proc_lib:init_ack(Parent, {ok, self()}),
    loop(...).

proc_lib:start_link是同步的,直到proc_lib:init_ack调用之后才会返回。

调试

为了支持sys的调试机制,需要用到 调试结构体 。Deb元组使用sys:debug_options/1初始化:

init(Parent) ->
    ...
    Deb = sys:debug_options([]),
    ...
    loop(Chs, Parent, Deb).

sys:debug_options/1需要选项列表作为参数。这里列表为空,表示最开始调试没有开启。更多可能的选项,查看STDLIB中 sys(3) 的man page。

对于每个需要被记录和跟踪的 system event,下面的函数被调用:

sys:handle_debug(Deb, Func, Info, Event) => Deb1

这里: * Deb是调试结构体 * Func是用于格式化输出信息的函数。对于每个系统事件,格式化函数以Func(Dev, Event, Info)的形式被调用,在这里: * Dev是IO设备,用于输出信息。查看 STDLIB 的 io(3) man page。 * Event和Info从handle_debug传入 * Info用于给Func传递更多信息,可以使任何原子。 * Event是系统事件。系统事件的定义和代表含义由用户定义。一般来说输入和输出消息被当做系统消息,以下面的形式{in, Msg[,From]}、{out, Msg, To}。

handle_debug返回更新的调试结构体 Deb1.

这个例子中,handle_debug会在消息发送和到达时被调用。格式化函数Func是ch4:write_debug/3,它简单的调用io:format/3输出调试信息。

loop(Chs, Parent, Deb) ->
    receive
        {From, alloc} ->
            Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,
            ch4, {in, alloc, From}),
            {Ch, Chs2} = alloc(Chs),
            From ! {ch4, Ch},
            Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}),
            loop(Chs2, Parent, Deb3);
        {free, Ch} ->
            Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, {free, Ch}}),
            Chs2 = free(Ch, Chs),
            loop(Chs2, Parent, Deb2);
        {system, From, Request} ->
            sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)
    end.

write_debug(Dev, Event, Name) ->
    io:format(Dev, "~p event = ~p~n", [Name, Event]).
处理系统消息

** 系统消息 ** 以下面的形式接收:

{system, From, Request}

消息内容和含义不需要被进程理解。相反,下面的函数被调用:

sys:handle_system_msg(Request, From, Parent, Module, Deb, State)

这个函数不会返回,它处理这个系统消息,如果进程继续执行,会调用下面的函数:

Module:system_continue(Parent, Deb, State)

或者终止进程:

Module:system_continue(Parent, Deb, State)

监控树中的进程应该和父进程以同样的原因终止:

  • Request 和 From 会被pass下去
  • Parent 是父进程的pid
  • Module 是模块名
  • Deb 是调试结构体
  • State 描述内部状态,会发送给system_continue/system_terminate/system_get_state/system_replace_state。

如果进程要获取它的状态,handle_system_msg调用:

Module:system_get_state(State)

如果进程要用StateFun替换它的状态,handle_system_msg调用:

Module:system_replace_state(StateFun, State)

这个例子中:

loop(Chs, Parent, Deb) ->
    receive
        ...
        {system, From, Request} ->
            sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)
    end.
    
system_continue(Parent, Deb, Chs) ->
    loop(Chs, Parent, Deb).
    
system_terminate(Reason, _Parent, _Deb, _Chs) ->
    exit(Reason).
    
system_get_state(Chs) ->
    {ok, Chs}.
    
system_replace_state(StateFun, Chs) ->
    NChs = StateFun(Chs),
    {ok, NChs, NChs}.

如果特殊进程设置为捕捉退出信号,它的父进程终止,预期的行为是以同样的原因终止:

init(...) ->
    ...,
    process_flag(trap_exit, true),
    ...,
    loop(...).
loop(...) ->
    receive
        ...
        {'EXIT', Parent, Reason} ->
            ..maybe some cleaning up here..
            exit(Reason);
        ...
    end
9.7.3 用户自定义行为模式

为了实现用户自定义行为模式,需要编写和之前的特殊进程类似的代码,但是所有的函数都在回调模块中,来处理具体任务。

如果想让编译器提醒模块缺少回调函数,可以类似OTP的行为模式,可以再行为模块中添加-callback属性,指定必须的回调函数:

-callback Name1(Arg1_1, Arg1_2, ..., Arg1_N1) -> Res1.
-callback Name2(Arg2_1, Arg2_2, ..., Arg2_N2) -> Res2.
...
-callback NameM(ArgM_1, ArgM_2, ..., ArgM_NM) -> ResM.

NameX是回调函数名,ArgX和ResX是类型描述。-callback属性支持完整的-spec属性。

可选的回调函数用-optional_callbacks属性指定:

-optional_callbacks([OptName1/OptArity1, ..., OptNameK/OptArityK]).

每个OptName/OptArity表明回调函数的名称和元数。注意optional_callbacks属性和-callback属性一起使用,不能联合下面介绍的behaviour_info()。

Behaviour:behaviour_info(optional_callbacks)可以获取所有可选的回调函数。

注意:我们推荐使用-callback属性而不是behaviour_info()函数。因为额外的类型信息可以用于文档生成工具或排查差异。

作为-callback和-optional_callbacks属性的替代,可以导出behaviour_info()函数:

behaviour_info(callbacks) ->
    [{Name1, Arity1},...,{NameN, ArityN}].

这个函数可以用-callback属性自动生成。

当编译器发现模块属性-behaviour(Behaviour)时。在模块Mod中,调用Behaviour:behaviour_info(callbacks)并比较实际导出的函数,如果缺少必要的回调会给出警告信息:

例子

%% User-defined behaviour module
-module(simple_server).
-export([start_link/2, init/3, ...]).
-callback init(State :: term()) -> 'ok'.
-callback handle_req(Req :: term(), State :: term()) -> {'ok', Reply :: term()}.
-callback terminate() -> 'ok'.
-callback format_state(State :: term()) -> term().
-optional_callbacks([format_state/1]).

%% Alternatively you may define:
%%
%% -export([behaviour_info/1]).
%% behaviour_info(callbacks) ->
%%     [{init,1}, {handle_req,2},{terminate,0}].
start_link(Name, Module) ->
    proc_lib:start_link(?MODULE, init, [self(), Name, Module]).
init(Parent, Name, Module) ->
    register(Name, self()),
    ...,
    Dbg = sys:debug_options([]),
    proc_lib:init_ack(Parent, {ok, self()}),
    loop(Parent, Module, Deb, ...).

在回调模块中:

-module(db).
-behaviour(simple_server).
-export([init/1, handle_req/2, terminate/0]).
...

行为模块中的-callback属性可以添加-spec重定义。如下:

-module(db).
-behaviour(simple_server).
-export([init/1, handle_req/2, terminate/0]).

-record(state, {field1 :: [atom()], field2 :: integer()}).
-type state()   ::  #state{}.
-type request() ::  {'store', term(), term()};
                    {'lookup', term()}.
...
-spec handle_req(request(), state()) -> {'ok', term()}.
...

每个spec约束是callback约束的子类型。