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 0023 理解Erlang/OTP gen_server

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,接口的实现部分放在回调模块实现.

Erlang 0023 理解Erlang/OTP gen_server

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


这个例子已经可以运行了

([email protected])1> server_demo:start().
<0.38.0>
([email protected])2> server_demo:alloc().
1
([email protected])3> server_demo:alloc().
2
([email protected])4> server_demo:alloc().
3
([email protected])5> server_demo:alloc().
4
([email protected])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是否存在,看下面的例子:

([email protected])33> is_process_alive(pid(0,222,0)).
false
([email protected])34> gen_server:cast(pid(0,222,0),hello).
ok
([email protected])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》

各位,晚安! 

 

更多相关文章
  • "让共享软件时间倒流" 现在有许多比较流的共享软件都有一个使用期限, 过了这个期限就再也不能使用该软件了, 这给用户带来了很大的不便于工便, 这次我要告诉大家一个方法,使得你不用反汇编软件, 甚至根本不跟踪目标软件的运行,只要根据它留下的"脚印"就可以让你突破 ...
  • 享元模式=单例模式+工厂模式+合成模式   单例模式: 保证一个类仅有一个实例,并提供一个访问它的全局访问点. 结构图:   注意多线程的单例. [java]  package com.bankht.Flyweight.complex;    /**  * @author: 特种兵—AK47  * ...
  • Android手势密码LockPatternView.LockPasswordUtils.LockPatternUtils 在使用别人写的这个手势密码的时候,我们通常是有自己的需求,可能这里的代码很多也很复杂,有没有什么很多注释,要把整个代码弄明白是要花很多时间而且基础要非常好的,可能在赶项目的时候 ...
  • 引言 HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢? 一 Entry Entry是Map接口中的一个内部接口,它是实现键值对存储关键.在HashMap中,有Entry的实现类,叫做Entry.Entry类很简 单,里面包含key,value,由外部引入的hash,还有指向 ...
  • 第四讲 点云拼接 广告:“一起做”系列的代码网址:https://github.com/gaoxiang12/rgbd-slam-tutorial-gx 当博客更新时代码也会随着更新. SLAM技术交流群:254787961 读者朋友们大家好!尽管还没到一周,我们的教程又继续更新了,因为暑假实在太闲 ...
  • “前程无忧”(Nasdaq: JOBS)是国内第一个集多种媒介资源优势的专业人力资源服务机构.它集合了传统媒体.网络媒体及先进的信息技术,加上一支经验丰富的专业顾问队伍,提供包括招聘猎头.培训测评和人事外包在内的全方位专业人力资源服务,现在全国25个城市设有服务机构.2004年9月,前程无忧成为第一 ...
一周排行