[Erlang 0023] 理解Erlang/OTP gen_server

简介:

  Erlang的OTP behaviour是对一些通用编程模式的抽象,在用Erlang 语言做开发时可以在behavior基础上快速构建出可用且可靠的功能.OTP behaviour包含gen_server gen_event gen_fsm supervisor.其中绝大多数情况下都是在使用gen_server,supervisor本身也是使用gen_server实现的.我们就以gen_server做为起点,逐步学习Erlang OTP.

 模式的模式 

   gen_server gen_event等等是基于进程来实现具有特定功能的模块,它们可以称之为面向功能的模式;Erlang的世界观是"一切皆为进程",进程是Erlang的基础设施,不管要完成什么功能,进程都会遵循通用的行为模式,这些进程模式模式可以称为模式的模式;我们直接使用进程做开发的时候会遵循这个模式,behavior的实现同样遵循进程模式.下面我们要看一下使用进程开发的共同点与不同点,了解共同点方便我们识别出模式和骨架(skeleton),了解不同点方便我们设计回调接口来获得灵活性.

 

   进程的共同点

   创建进程的时候会有哪些通用操作?①注册别名,②新创建的进程首先会初始化进程的循环状态数据(loop data).循环状态数据存放在变量中,这个变量被成为进程状态(process state).③进程状态会传递给loop方法.进程在运行状态通过loop方法来实现状态循环,loop方法的职责就是receive-evaluate,接受到消息,处理,更新进程状态,并把进程状态作为尾递归的参数传递回去.如果进程接收到stop消息,进程就进行清理并终止.

   进程之间的差异

    ①创建进程传递给spawn的参数不同 ②是否要注册别名,注册哪种别名 ③进程的初始化化过程会因进程的功能不同而异 ④存储进程状态这一处理是通用的,但是进程状态在不同进程是不拘一格的; ⑤receive-evaluate loop方法是通用的,但是接受的消息和具体的处理行为不尽相同 ⑥进程终止清理逻辑也都不同 

 

   袋鼠书《Erlang Programming》里面的有一张图很好的描述了进程通用模型:

    

Erlang/OTP gen_server

Behaviours are formalizations of these common patterns. The idea is to divide the code for a process in a generic part (a behaviour module) and a specific part (a callback module).

behavior是进程模式的规范化,把代码分成两部分,一部分是通用部分(behavior模块),一部分是定制部分(回调模块).对于gen_server就是要把client/server的模型进行一个抽象和封装,把behavior和回调模块需要完成的职责分离开;我们上面分析过了进程的共同点和差异,显然共同点要放在behavior中,差异的部分要放在回调模块中实现.另外,针对client/server模型,功能上的共同点就是会有阻塞调用和非阻塞调用两种,那么这两种调用的接口是通用的,但是具体接口的实现是不同的,因此接口部分定义在behavior,接口的实现部分放在回调模块实现.

复制代码
-module(server_template).

-export([start/1]).
-export([call/2, cast/2]).
-export([init/1]).

%%通用进程模式
start(Mod) ->
spawn(server_template, init, [Mod]).

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);
stop ->
stop
end.

%% 接口部分
call(Name, Req) ->
Name ! {call, self(), Req},
receive
{Name, Res} ->
Res
end.
cast(Name, Req) ->
Name ! {cast, Req},
ok.
复制代码

这样一个gen_server的代码骨架就出来了,甚至现在已经可以基于这个简单的骨架来实现一个server了.我们就使用OTP文档常用的alloc_free的例子:

复制代码
-module(server_demo).
-export([start/0]).
-export([alloc/0, free/1]).
-export([init/0, handle_call/2, handle_cast/2]).

start() ->
server_template:start(server_demo).

alloc() ->
server_template:call(server_demo, alloc).
free(Ch) ->
server_template:cast(server_demo, {free, Ch}).

init() ->
channels().

handle_call(alloc, Chs) ->
alloc(Chs).
handle_cast({free, Ch}, Chs) ->
free(Ch, Chs).

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.
复制代码


这个例子已经可以运行了

(demo@192.168.1.123)1> server_demo:start().
<0.38.0>
(demo@192.168.1.123)2> server_demo:alloc().
1
(demo@192.168.1.123)3> server_demo:alloc().
2
(demo@192.168.1.123)4> server_demo:alloc().
3
(demo@192.168.1.123)5> server_demo:alloc().
4
(demo@192.168.1.123)6> server_demo:free(1).
ok

 

 下面是一个gen_server的代码模板,可以看到这个模板实际上是给出了我们需要完成的回调函数;与上面简陋的demo不同的是,下面的模板包含更多可以定制的内容:

  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   
复制代码
%%gen_server代码模板

-module(new_file).

-behaviour(gen_server).
% --------------------------------------------------------------------
% Include files
% --------------------------------------------------------------------

% --------------------------------------------------------------------
% External exports
-export([]).

% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-record(state, {}).



% --------------------------------------------------------------------
% Function: init/1
% Description: Initiates the server
% Returns: {ok, State} |
% {ok, State, Timeout} |
% ignore |
% {stop, Reason}
% --------------------------------------------------------------------
init([]) ->
{ok, #state{}}.

% --------------------------------------------------------------------
% Function: handle_call/3
% Description: Handling call messages
% Returns: {reply, Reply, State} |
% {reply, Reply, State, Timeout} |
% {noreply, State} |
% {noreply, State, Timeout} |
% {stop, Reason, Reply, State} | (terminate/2 is called)
% {stop, Reason, State} (terminate/2 is called)
% --------------------------------------------------------------------
handle_call(Request, From, State) ->
Reply = ok,
{reply, Reply, State}.

% --------------------------------------------------------------------
% Function: handle_cast/2
% Description: Handling cast messages
% Returns: {noreply, State} |
% {noreply, State, Timeout} |
% {stop, Reason, State} (terminate/2 is called)
% --------------------------------------------------------------------
handle_cast(Msg, State) ->
{noreply, State}.

% --------------------------------------------------------------------
% Function: handle_info/2
% Description: Handling all non call/cast messages
% Returns: {noreply, State} |
% {noreply, State, Timeout} |
% {stop, Reason, State} (terminate/2 is called)
% --------------------------------------------------------------------
handle_info(Info, State) ->
{noreply, State}.

% --------------------------------------------------------------------
% Function: terminate/2
% Description: Shutdown the server
% Returns: any (ignored by gen_server)
% --------------------------------------------------------------------
terminate(Reason, State) ->
ok.

% --------------------------------------------------------------------
% Func: code_change/3
% Purpose: Convert process state when code is changed
% Returns: {ok, NewState}
% --------------------------------------------------------------------
code_change(OldVsn, State, Extra) ->
{ok, State}.
复制代码

 

进程如何启动

通常实现一个gen_server的应用,我们会暴露出来start,start_link方法,start是为了启动独立的(stand_alone)gen_server,start_link是用于在监控树中启动gen_server;start/start_link方法会①调用指定的init/1函数,返回值是{ok,State};②指定是否注册name,以及注册何种name,是{local, Name}还是 {global, Name} ③指定gen_server启动选项,这个启动选项是何种格式?其实就是spawn_opt,比如{spawn_opt,[{fullsweep_after,5000},{min_heap_size, 1000}]};我曾经介绍过proc_lib http://www.cnblogs.com/me-sa/archive/2011/11/22/erlang0017.html 要查看spawn_opt所有可用的配置项? 点击这里:http://www.erlang.org/doc/man/erlang.html#spawn_opt-4

 gen_server:start(Mod, Args, Options)
 gen_server:start(Name, Mod, Args, Options)
 gen_server:start_link(Mod, Args, Options)
 gen_server:start_link(Name, Mod, Args, Options)

注意上面的启动过程都是同步的,需要完成初始化之后才开始接收请求,这个[Erlang 0017]Erlang/OTP基础模块 proc_lib 我们已经讨论过是如何实现的;注意gen_server并没有自动进行trap_exit,如果需要就要在init函数中添加.同时,有些gen_server在启动的时候可能需要较长的时间,这个可以通过定制{timeout,10000}参数来实现,默认值是5000ms.

 

如何处理同步请求 Synchronous Requests - Call

 gen_server:call(ServerRef, Request) -> Reply
 gen_server:call(ServerRef, Request, Timeout) -> Reply
这里的ServerRef 可以是 Name | {Name,Node} | {global,GlobalName} | pid(),由于是阻塞调用,所以要么调用成功返回要么超时返回;我们可以针对特定的请求设定超时的值,做一个更细粒度的超时控制;

 

如何处理异步请求 Asynchronous Requests - Cast

cast(ServerRef, Request) -> ok

这里要注意的是异步请求会立即返回ok不管目的地节点/gen_server是否存在,看下面的例子:

(erl@192.168.1.123)33> is_process_alive(pid(0,222,0)).
false
(erl@192.168.1.123)34> gen_server:cast(pid(0,222,0),hello).
ok
(erl@192.168.1.123)35> gen_server:call(pid(0,222,0),hello).
** exception exit: {noproc,{gen_server,call,[false,hello]}}
     in function  gen_server:call/2 (gen_server.erl, line 180)

 

通用消息处理 handle_info

This function is called by a gen_server when a timeout occurs or when it receives any other message than a synchronous
or asynchronous request (or a system message).

这个方法的定位是处理同步请求,异步请求之外的消息,如果一个消息我们能明确的知道是call还是cast就不要走这里;常见的是用它来接收退出消息:

handle_info({'EXIT', Pid, Reason}, State) ->

    ..code to handle exits here..
    {noreply, State1}.

 

如何处理进程终止

   如果gen_server在监控树中不需要stop函数,gen_server会由其supervisor根据shutdown策略自动终止掉.如果要在进程终止之前执行清理,shutdown策略必须设定一个timeout值而不是brutal_kill并且gen_server要在init设置trap_exit.当被supervisor命令shutdown的时候,gen_server会调用terminnate(shutdown,State),特别注意: 被supervisor终止掉,终止的原因是Reason=shutdown,这个我们之前也

复制代码
init(Args) ->
...,
process_flag(trap_exit, true),
...,
{ok, State}.
...
terminate(shutdown, State) ->
..code for cleaning up here..
ok.
复制代码


如果gen_server不是supervisor的一部分,stop方法就很有用了:

复制代码
...
export([stop/0]).
...
stop() ->
gen_server:cast(ch3, stop).
...
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast({free, Ch}, State) ->
....
...
terminate(normal, State) ->
ok.
复制代码



通过调用terminate方法,gen_server可以优雅的关闭掉了. 如果结束的消息不是normal,shutdowngen_server就会被认为是异常终止并通过error_logger:format/2产生错误报告.

Note: if any reason other than normalshutdown or {shutdown, Term} is used whenterminate/2 is called, the OTP framework will see this as a failure and start logging a bunch of stuff here and there for you.

 

好了,就到这里,休息一下


 

劳逸结合每次推荐一张唱片,今天推荐《台湾百佳唱片》第一张,1982年滚石唱片出品 罗大佑《之乎者也》

1.鹿港小镇 2.恋曲1980 3.童年4.错误 5.摇篮曲 6.之乎者也 7.乡愁四韵 8.将进酒 9.光阴的故事 10.蒲公英

    遥远的路程昨日的梦以及远去的笑声
再次的见面我们又历经了多少的路程
熟悉的旧日熟悉的你有着旧日狂热的梦
不再是旧日熟悉的我有着依然的笑容  --《光阴的故事》

     

    你曾经对我说 你永远爱着我
爱情这东西我明白 但永远是什么
姑娘你别哭泣 我俩还在一起
今天的欢乐 将是明天永恒的回忆  --《恋曲1980》

目录
相关文章
|
20天前
|
网络协议 Linux Go
分享一个go开发的工具-SNMP Server
分享一个go开发的工具-SNMP Server
24 0
[Erlang 0127] Term sharing in Erlang/OTP 上篇
之前,在 [Erlang 0126] 我们读过的Erlang论文 提到过下面这篇论文:  On Preserving Term Sharing in the Erlang Virtual Machine地址: http://user.
1283 0
|
Shell 自然语言处理 网络协议