[转]理解gen_server behaviour

在Erlang/OTP中有一个基本概念叫监督树。这是一种建立在督程佣程思想上的进程结构化模型。
  • 佣程(worker)是进行计算的进程,也就是说,它们进行实际的工作。
  • 督程(supervisor)是监视工作者行为的进程。监督者可以重启工作者如果出现了什么问题.
  • 监督树是一种将代码分成监督者和工作者的层次安排,这样才能设计和编写可容错的软件。
_images/sup6.gif

上图中,方框提供监督,圆圈是工作者。

行为

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

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

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

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

%%ch2.erl:

-module(ch2).
-export([start/0]).
-export([alloc/0, free/1]).
-export([init/0, handle_call/2, handle_cast/2]).

start() ->
    server:start(ch2).

alloc() ->
    server:call(ch2, alloc).

free(Ch) ->
    server:cast(ch2, {free, Ch}).

init() ->
    channels().

handle_call(alloc, Chs) ->
    alloc(Chs). % => {Ch,Chs2}

handle_cast({free, Ch}, Chs) ->
    free(Ch, Chs). % => Chs2
 
channels() ->
   {_Allocated = [], _Free = lists:seq(1,100)}.

alloc({Allocated, [H|T] = _Free}) ->
   {H, {[H|Allocated], T}}.

free(Ch, {Alloc, Free} = Channels) ->
   case lists:member(Ch, Alloc) of
      true ->
         {lists:delete(Ch, Alloc), [Ch|Free]};
      false ->
         Channels
   end.

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

-module(server).
-export([start/1]).
-export([call/2, cast/2]).
-export([init/1]).
start(Mod) ->
    spawn(server, init, [Mod]).
call(Name, Req) ->
    Name ! {call, self(), Req},
    receive
        {Name, Res} ->
            Res
    end.
cast(Name, Req) ->
    Name ! {cast, Req},
    ok.
init(Mod) ->
    register(Mod, self()),
    State = Mod:init(),
    loop(Mod, State).
loop(Mod, State) ->
    receive
        {call, From, Req} ->
            {Res, State2} = Mod:handle_call(Req, State),
            From ! {Mod, Res},
            loop(Mod, State2);
        {cast, Req} ->
            State2 = Mod:handle_cast(Req, State),
            loop(Mod, State2)
    end.
注意以下几点:
  • server 中的代码可以被重用于建立很多不同的服务器端。
  • 服务器的名字——这个例子中为原子 ch2——对于客户端函数的用户而言是隐藏的。这意味无须影响客户端就可以改变名字。
  • 协议(发给服务器和从服务器接收到的消息)也是隐藏的。这是很好的编程实践,让我们可以在不改变接口函数的代码的情况下改变协议。
  • 我们可以扩展服务器 server 的功能,而不用改变 ch2 或任何其它的回调模块。

 ---------------------------------------------------------------

这个服务器可以用 gen_server 进行重写,结果产生这个回调模块:

-module(ch3).
-behaviour(gen_server).

-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ch3}, ch3, [], []).

alloc() ->
    gen_server:call(ch3, alloc).

free(Ch) ->
    gen_server:cast(ch3, {free, Ch}).

init(_Args) ->
    {ok, channels()}.

handle_call(alloc, _From, Chs) ->
    {Ch, Chs2} = alloc(Chs),
    {reply, Ch, Chs2}.

handle_cast({free, Ch}, Chs) ->
    Chs2 = free(Ch, Chs),
    {noreply, Chs2}.
-------------------------------------------------------------------

gen_server 的交互序列

  1. 进程如何启动
  2. 如何处理同步请求 Synchronous Requests - Call
  3. 如何处理异步请求 Asynchronous Requests - Cast
  4. 通用消息处理 handle_info
  5. 如何处理进程终止
  6. 如何进行代码版本替换
    gen_server module            Callback module
    -----------------                       
    ---------------
    gen_server:start_link -----> Module:init/1
    gen_server:call
    gen_server:multi_call -----> Module:handle_call/3
    gen_server:cast
    gen_server:abcast     -----> Module:handle_cast/2
    -                     -----> Module:handle_info/2
    -                     -----> Module:terminate/2
    -                     -----> Module:code_change/3   
 
原文地址:https://www.cnblogs.com/freebird92/p/2302312.html