理解gen_server behaviour

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

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

行为

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

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

行为模块是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   

 

 

你可能感兴趣的:(server)