模式的模式
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不同的是,下面的模板包含更多可以定制的内容:
- 进程如何启动
- 如何处理同步请求 Synchronous Requests - Call
- 如何处理异步请求 Asynchronous Requests - Cast
- 通用消息处理 handle_info
- 如何处理进程终止
- 如何进行代码版本替换
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
normal
,shutdown
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》