• 行为

    行为

    在监督树中,很多进程有着相似结构,遵循类似的模式。例如,督程的结构都很相似。他们之间的唯一区别在于所监督的子进程。此外,很多佣程都是处于服务器-客户端关系中的服务器,有限状态机或者诸如错误日志这样的事件处理器。

    行为是对这些常见模式的形式化。其思想是将一个进程的代码划分为一个通用的部分(行为模块)和一个特定的部分(回调模块)。

    行为模块是Erlang/OTP的一部分。要实现一个督程,用户只需要实现回调模块,导出预定义集合中的函数—— 回调函数

    一个例子可以用来说明代码是如何被划分成为通用和特定部分的:考虑下面的代码(普通Erlang编写),一个简单的服务器,用于保持跟踪一些“频道”。其他进程可以通过调用 alloc/0free/1 函数来相应地分配和释放一个频道。

    1. -module(ch1).
    2. -export([start/0]).
    3. -export([alloc/0, free/1]).
    4. -export([init/0]).
    5. start() ->
    6. spawn(ch1, init, []).
    7. alloc() ->
    8. ch1 ! {self(), alloc},
    9. receive
    10. {ch1, Res} ->
    11. Res
    12. end.
    13. free(Ch) ->
    14. ch1 ! {free, Ch},
    15. ok.
    16. init() ->
    17. register(ch1, self()),
    18. Chs = channels(),
    19. loop(Chs).
    20. loop(Chs) ->
    21. receive
    22. {From, alloc} ->
    23. {Ch, Chs2} = alloc(Chs),
    24. From ! {ch1, Ch},
    25. loop(Chs2);
    26. {free, Ch} ->
    27. Chs2 = free(Ch, Chs),
    28. loop(Chs2)
    29. end.

    服务器的代码可以重写为一个通用的部分 server.erl:

    1. -module(server).
    2. -export([start/1]).
    3. -export([call/2, cast/2]).
    4. -export([init/1]).
    5. start(Mod) ->
    6. spawn(server, init, [Mod]).
    7. call(Name, Req) ->
    8. Name ! {call, self(), Req},
    9. receive
    10. {Name, Res} ->
    11. Res
    12. end.
    13. cast(Name, Req) ->
    14. Name ! {cast, Req},
    15. ok.
    16. init(Mod) ->
    17. register(Mod, self()),
    18. State = Mod:init(),
    19. loop(Mod, State).
    20. loop(Mod, State) ->
    21. receive
    22. {call, From, Req} ->
    23. {Res, State2} = Mod:handle_call(Req, State),
    24. From ! {Mod, Res},
    25. loop(Mod, State2);
    26. {cast, Req} ->
    27. State2 = Mod:handle_cast(Req, State),
    28. loop(Mod, State2)
    29. end.

    还有一个回调模块 ch2.erl:

    1. -module(ch2).
    2. -export([start/0]).
    3. -export([alloc/0, free/1]).
    4. -export([init/0, handle_call/2, handle_cast/2]).
    5.  
    6. start() ->
    7. server:start(ch2).
    8.  
    9. alloc() ->
    10. server:call(ch2, alloc).
    11.  
    12. free(Ch) ->
    13. server:cast(ch2, {free, Ch}).
    14.  
    15. init() ->
    16. channels().
    17.  
    18. handle_call(alloc, Chs) ->
    19. alloc(Chs). % => {Ch,Chs2}
    20.  
    21. handle_cast({free, Ch}, Chs) ->
    22. free(Ch, Chs). % => Chs2
    • 注意以下几点:
      • server 中的代码可以被重用于建立很多不同的服务器端。
      • 服务器的名字——这个例子中为原子 ch2 ——对于客户端函数的用户而言是隐藏的。这意味无须影响客户端就可以改变名字。
      • 协议(发给服务器和从服务器接收到的消息)也是隐藏的。这是很好的编程实践,让我们可以在不改变接口函数的代码的情况下改变协议。
      • 我们可以扩展服务器 server 的功能,而不用改变 ch2 或任何其它的回调模块。(在上面的 ch1.erlch2.erl 中, channels/0alloc/1free/2 的实现被特意省略了,因为和这个例子无关。为了完整起见,下面会给出这些函数的一种写法。注意这只是个例子,实际的实现必须能够处理一些特殊情况,如频道用光无法分配等)
    1. channels() ->
    2. {_Allocated = [], _Free = lists:seq(1,100)}.
    3.  
    4. alloc({Allocated, [H|T] = _Free}) ->
    5. {H, {[H|Allocated], T}}.
    6.  
    7. free(Ch, {Alloc, Free} = Channels) ->
    8. case lists:member(Ch, Alloc) of
    9. true ->
    10. {lists:delete(Ch, Alloc), [Ch|Free]};
    11. false ->
    12. Channels
    13. end.

    不用行为来写的代码可能会更快些,但是所提高的效率是要付出通用性上的代价。对系统中以一致的方式管理所有应用的能力是非常重要的。

    使用行为也可以使得由其他程序员所写的代码更容易阅读和理解。专门的编程结构,可能会更高效,但是往往更加难于理解。

    模块 server 对应的是极大简化了的Erlang/OTP中的 gen_server 行为。

    标准 Erlang/OTP 行为有:

    • gen_server
    • 用于实现 C/S 结构中的服务端。
    • gen_fsm
    • 用于实现有限状态机。
    • gen_event
    • 用于实现事件处理功能。
    • supervisor
    • 用于实现监督树中的督程。编译器看到模块属性 -behaviour(Behaviour). 后会提出缺少的回调函数的警告。例如:
    1. -module(chs3).
    2. -behaviour(gen_server).
    3. ...
    4.  
    5. 3> c(chs3).
    6. ./chs3.erl:10: Warning: undefined call-back function handle_call/3
    7. {ok,chs3}