Gen_server行为分析与实践

时间:2022-02-25 00:28:11

1.简介

Gen_server实现了通用服务器client_server原理,几个不同的客户端 分享服务端管理的资源(如图),gen_server提供标准的接口函数和包含追踪功能以及错误报告来实现通用的服务器,同时可以作为OTP监控树的一部分。
Gen_server行为分析与实践
Gen_server函数与回调函数之间的关系:
  1.  1 gen_server moduleCallbackmodule
    2 --------------------------------
    3 gen_server:start_link ----->Module:init/1
    4 gen_server:call
    5 gen_server:multi_call ----->Module:handle_call/3
    6 gen_server:cast
    7 gen_server:abcast ----->Module:handle_cast/2
    8 ------>Module:handle_info/2
    9 ------>Module:terminate/2
    10 ------>Module:code_change/3
如果回调函数失败或者是返回bad value,gen_server将终止。
Gen_server可以处理来自系统的消息,通过sys模块可以调试一个gen_server.(未实践)
注意:一个gen_server不能自动的捕获exit信号,必须在回调Module:init时设置process_flag(trap_exit,true)(实例3.2).
如果请求的gen_server不存在或参数是bad arguments,那么gen_server的所有请求都将fail.
如果回调函数中指定了hibernate,那么gen_server进程将进入hibernate,这对于一个长时间的空闲的进程非常有用,因为可以进行垃圾回收和减少内存占用。但是,要非常小心的使用hibernate,因为在hibernate到wake_up之间,至少有两个垃圾回收器,对于一个请求频繁的server不划算。

2.函数

2.1 导出函数
start(Module, Args, Options) -> Result
start(ServerName, Module, Args, Options) -> Result
start_link(ServerName, Module, Args, Options) -> Result
start_link(Module, Args, Options) -> Result
start_link与start的区别是:1.start用于创建一个独立的gen_server, 但是可以通过参数{spawn_opt,[link]}来达到start_link的效果; 2. start_link用于创建一个在监控树下的gen_server
对其参数的解析:
ServerName = {local,Name} | {global,GlobalName} | {via,Module,ViaName}
{local,Name}:在本地节点命名为Name,通过register/2
{debug,Dbgs}回去调用sys模块中指定的方法
Dbgs = [ trace | log | statistics | {log_to_file,FileName} | {install,{Func,FuncState}} ]
{global,Name}:在全局命名为Name,通过global:register_name/2
{via,Module,ViaName}:通过Module来命名,其原理象global,必须导出register_name/2,unregister_name/1whereis_name/1 , send/2等函数
如果没有该参数,则用pid()来作为名字
Module:回调模块
Args:是回调方法init的参数
Options:[ {debug,Dbgs} | {timeout,Time} | {spawn_opt,SOpts}]
{timeout,Time}是初始化的的时间限制,超出时间限制返回{error,timeout}
{spawn_opt,SOpt}是在生成gen_server可以设置的参数,通过内建函数spawn_opt来实现
SOpt = Option = link  %% 与创建的gen_server连接
| monitor  %% 在这里会报错
| {priority, Level :: priority_level()} %%设置优先级
| {fullsweep_after, Number :: integer() >= 0} %%多长时间进行一次全局扫描,进行垃圾回收
| {min_heap_size, Size :: integer() >= 0} %%最小堆内存
| {min_bin_vheap_size, VSize :: integer() >= 0} %%最小二进制虚拟堆内存
priority_level() = low | normal | high | max
如果创建gen_server成功返回{ok, Pid()},如果创建的进程已经存在返回 {error,{already_started,Pid}}
如果回调函数init失败返回{error, Reason},如果回调函数返回{stop,Reason}或ignore则返回{error,Reason}或ignore

call(ServerRef, Request) -> Reply
call(ServerRef, Request, Timeout) -> Reply
ServerRef = Name | {Name,Node} | {global,GlobalName} | {via,Module,ViaName} | pid()
Name:本地注册名称为Name的gen_server.
{Name,Node}:调用Node节点上注册名称为Name的gen_server.
{global,GlobalName}:调用全局名称为GlobalName的gen_server.
{via,Module,ViaName}:调用注册在Module名称为ViaName的gen_server.(原理同global)
pid():调用进程pid上的gen_server.
Timeout = int()>0 | infinity
           同步调用的时间限制,infinity表示无穷大,默认为5000毫秒
          当超时时就会发生错误,如果捕获该错误,将继续运行
multi_call(Name, Request) -> Result                                %%会调用所有节点
multi_call(Nodes, Name, Request) -> Result
multi_call(Nodes, Name, Request, Timeout) -> Result         %%调用指定列表节点,并有时间限制
该函数的作用是对指定节点列表上本地注册名称是Name的gen_server发起请求,然后等待返回
其回调函数为Module:handle_call/3
Nodes = [Node]
节点列表
Timeout = int()>=0 | infinity
同步调用的时间限制,infinity表示无穷大,默认为 infinity 毫秒
如果在指定的时间限制内未返回,该节点为BadNodes
注意:对于非Erlang节点等待可能无穷大,例如Java或C节点。(未验证)
Result = {Replies,BadNodes}
Replies = [{Node,Reply}]
BadNodes = [Node]
没有响应的节点列表。

cast(ServerRef, Request) -> ok
ServerRef = Name | {Name,Node} | {global,GlobalName} | {via,Module,ViaName} | pid()
参数信息和gen_server:call同义
发送一个异步请求给Module:handle_cast处理,并立即返回ok.如果节点或gen_server不存在请求将被ignore.
abcast(Name, Request) -> abcast
abcast(Nodes, Name, Request) -> abcast
发送一个异步请求给指定节点并且本地注册名称为Name的gen_server,并立即返回abcast.如果节点不存在或者Name不存在,请求将被忽略掉ignore.

reply(Client, Reply) -> Result
Client - see below
该函数用于gen_server向一个指定的客户端发送信息,但是,请求函数call or multi_call的回调函数Module:handle_call是没有定义Reply.
客户端必须是回调函数提供的From,Reply是任意的数据结构作为call or multi_call的返回值。

enter_loop(Module, Options, State)
enter_loop(Module, Options, State, ServerName)
enter_loop(Module, Options, State, Timeout)
enter_loop(Module, Options, State, ServerName, Timeout)
Options = [Option]
Option = {debug,Dbgs}
Dbgs = [trace | log | statistics | {log_to_file,FileName} | {install,{FuncState}} ]
ServerName = {local,Name} | {global,GlobalName} | {via,Module,ViaName}
Timeout = int() | infinity
该函数的功能是让一个已经存在的进程成为gen_server进程,通过请求进程让它进入gen_server循环成为gen_server进程,该普通进程的创建必须通过proc_lib模块(实例3.4)。
这个函数更加的有用对于要进行比gen_server还要复杂的初始化时。
Module Options  and  ServerName  的意义与gen_server:start_link一样.然而,如果Servername被指定这个进程被调用前一定要相应的先注册。
State and Timeout与回调函数的Module:init一样。
Failure: 如果请求进程不是一个由proc_lib创建的进程或使用了没有注册的ServerName.
2.2 回调函数
Module:init(Args) -> Result
Types:
Result = {ok,State} | {ok,State,Timeout} | {ok,State,hibernate}| {stop,Reason} | ignore
State = term()
Timeout = int()>=0 | infinity
通过start或start_link初始化一个新的进程。
若初始化成功返回{ok,State} | {ok,State,Timeout} | {ok,State,hibernate}。State是gen_server的内部状态;Timeout指进程初始化后等待接受请求的时间限制,超过时间限制将向handle_info发送请求为timeout的信息,默认是infinity;hibernate指可通过调用proc_lib:hibernate/3使进程进入冬眠状态从而进行GC,当有消息请求该进程,处理该请求,然后冬眠并进行GC.注意应该小心使用'hibernate',主要针对空闲时间比较长的进程,因为至少有两个GC回收器,对于请求比较平凡的进程,资源的消耗高。
如果初始化失败将返回{stop,Reason} | ignore

Module:handle_call(Request, From, State) -> Result
Types:
From = {pid(),Tag}
Result = {reply,Reply,NewState} | {reply,Reply,NewState,Timeout}
  | {reply,Reply,NewState,hibernate}
  | {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {stop,Reason,Reply,NewState} | {stop,Reason,NewState}
 Timeout = int()>=0 | infinity
处理call或multi_call的请求。
若返回 {reply,Reply,NewState} | {reply,Reply,NewState,Timeout}  | {reply,Reply,NewState,hibernate},Reply将返回给请求函数call or multi_call.Timeout | hibernate的意义与Module:init中意义相同。
若返回 {noreply,NewState} | {noreply,NewState,Timeout}  | {noreply,NewState,hibernate},gen_server将继续执行但没有返回,若要返回需要显示的调用gen_server:reply/2来返回。
若返回{stop,Reason,Reply,NewState} | {stop,Reason,NewState} ,前者的Reply将返回给调用函数,后者没有返回,若要返回显示调用gen_server:reply/2;两者最终都将调用Module:terminate/2来终止进程。

Module:handle_cast(Request, State) -> Result
Types:
Result = {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {stop,Reason,NewState}
 Timeout = int()>=0 | infinity
处理cast or abcast的请求。
其参数的描述信息与Module:handle_call中的一致。

Module:handle_info(Info, State) -> Result
Types:
Info = timeout | term()
Result = {noreply,NewState} | {noreply,NewState,Timeout} 
  | {noreply,NewState,hibernate}
  | {stop,Reason,NewState}
 Timeout = int()>=0 | infinity
 Reason = normal | term()
处理同步或异步异步请求的timeout信息,以及receive的信息。
其参数的描述信息与Module:handle_call中的一致。

Module:terminate(Reason, State)
Types:
Reason = normal | shutdown | {shutdown,term()} | term()
State = term()
这个函数在gen_server终止时被调用。它和Module:init是相对的可以在终止前进行一些清理工作。当gen_server终止的Reason返回时,这个返回将被ignore.
终止的Reason依赖于为什么终止,如果它是因为回调函数返回一个stop元组{stop, ...}那么终止Reason就是指定的终止原因;如果是由于失败(failure),则Reason是error原因。如果gen_server是监控树的一部分,被它的监控树有序的终止并且满足,1.被设置成可捕获的退出信号;2.关闭策略被设置成一个整数的timeout,而不是brutal_kill.则它的Reason是shutdown。甚至gen_server并不是监控树的一部分,只要它从父进程接收到'EXIT'消息,则Reason则是'EXIT'。
注意:无论由于任何原因[ normal | shutdown | {shutdown,term()} | term()] 终止,终止原因都是由于一个error或一个error report issued(error_logger:format/2) 

 
Module:code_change(OldVsn, State, Extra) -> {ok, NewState} | {error, Reason}
该函数主要用于版本的热更新,后续相关专题会介绍。

3.实例

3.1 请求
一 同步请求
通过gen_server:call或gen_server:multi_call发起同步请求,然后等待返回。
  1.  1 add1(Num1,Num2)->
    2 io:format("~nsync start~n"),
    3 Res= gen_server:call(?SERVER,{add1,Num1,Num2}),%%同步请求
    4 io:format("~nsync end~n"),
    5 Res.
    6 handle_call({add1,Num1,Num2},_From,State)->%%消息的接受处理方式
    7 Num=Num1+Num2,
    8 timer:sleep(3000),
    9 io:format("sleep end~n"),
    10 {reply,{ok, add1,Num},State}.
二 异步请求
通过gen_server:cast或gen_server:abcast发起异步请求立即返回ok|abcast,如果节点|gen_server|Name不存在请求ignore.
  1.  1 add2(Num1,Num2)->
    2 io:format("~nasync start~n"),
    3 Res= gen_server:cast(?SERVER,{add2,Num1,Num2}),%%异步请求
    4 io:format("~nasync end~n"),
    5 Res.
    6 handle_cast({add2,Num1,Num2},State)->%%消息的接受处理方式:
    7 Num=Num1+Num2,
    8 io:format("~n~p~n",[Num]),
    9 timer:sleep(3000),
    10 io:format("sleep end~n"),
    11 {noreply,State}.
三 其他消息处理
异步接收发送过来的消息,并进行相应的处理。例如在回调函数中返回{ok,State,Timeout},如果超时就会发送一条timeout消息gen_server(实例3.5).
  1.  1 add3(Num1,Num2)->
    2 io:format("~nsend start~n"),
    3 Res= erlang:send(?SERVER,{add3,Num1,Num2}),%%进程消息发送(异步)
    4 io:format("~nsend end~n"),
    5 Res.
    6 handle_info({add3,Num1,Num2},State)->%%消息的接受处理方式
    7 Num=Num1+Num2,
    8 io:format("~n~p~n",[Num]),
    9 timer:sleep(3000),
    10 io:format("sleep end~n"),
    11 {noreply,State}.
3.2 捕获异常消息
通过在gen_server初始化时设置process_flag(trap_exit,true)可以捕获本进程的退出消息。注意:要捕获exit就要进程之间要建立连接。
  1. 1 exit(Msg)->
    2 link(whereis(?MODULE)),
    3 erlang:exit(Msg).
    4 handle_info({'EXIT',From,Reson},State)->
    5 io:format("~p~p~n",[From,Reson]),
    6 {noreply,State};
    7 handle_info(_Info,State)->
    8 {noreply,State}.
调用结果:
Gen_server行为分析与实践
3.3 gen_server:reply/2
当Modile: handle_call没有返回时{noreply,NewState}, 可以通过gen_server:reply(Client, Reply)来返回给gen_server:call调用端。
  1.  1 add10(Num1,Num2)->
    2 io:format("~nsync start~n"),
    3 Res= gen_server:call(?SERVER,{noreply,Num1,Num2}),
    4 io:format("~nsync end~n"),
    5 Res.
    6 handle_call({noreply,Num1,Num2},_From,State)->
    7 Num=Num1+Num2,
    8 timer:sleep(3000),
    9 io:format("sleep end~n"),
    10 gen_server:reply(_From,{ok, gen_server_reply,Num}),%%给gen_server返回Replay
    11 {noreply,State};%%无返回
调用端返回:
Gen_server行为分析与实践
3.4 gen_server:enter_loop/3
gen_server:enter_loop方法可以让一个存在的普通进程成为一个gen_server进程。
  1.  1 -module(enter).
    2 -author("EricLw").
    3 %% API
    4 -export([start_link/0, init/1]).
    5 %% API
    6 -export([ add1/2]).
    7 %% gen_server callbacks
    8 -export([
    9 handle_call/3,
    10 handle_cast/2,
    11 handle_info/2,
    12 terminate/2,
    13 code_change/3]).
    14 -define(SERVER,?MODULE).
    15 -record(state,{}).
    16 %%%===================================================================
    17 %%% API
    18 %%%===================================================================
    19 start_link()->
    20 proc_lib:start_link(?SERVER, init,[self()]).%%通过proc_link来穿件普通进程
    21 add1(Num1,Num2)->
    22 io:format("~nsync start~n"),
    23 Res= gen_server:call(?SERVER,{add1,Num1,Num2}),
    24 io:format("~nsync end~n"),
    25 Res.
    26 init(Person)->
    27 proc_lib:init_ack(Person,{ok,self()}),
    28 register(?MODULE,self()),
    29 gen_server:enter_loop(?MODULE,[],#state{},{local,?MODULE}). %%指定了ServerName必须先注册Name
    30 handle_call({add1,Num1,Num2},_From,State)->
    31 Num=Num1+Num2,
    32 timer:sleep(3000),
    33 io:format("sleep end~n"),
    34 {reply,{ok, add1,Num},State};
    35 handle_call(_Request,_From,State)->
    36 {reply, ok,State}.
    37
    38 handle_cast(_Request,State)->
    39 {noreply,State}.
    40
    41 handle_info(_Info,State)->
    42 {noreply,State}.
    43 terminate(_Reason,_State)->
    44 timer:sleep(3000),
    45 io:format("~nclean up~n"),
    46 ok.
    47 code_change(_OldVsn,State,_Extra)->
    48 {ok,State}.
注意: 如果Servername被指定这个进程被调用前一定要相应的先注册。
3.5 timeout以及hibernate
在gen_server的回调函数中如init,handle_call,handle_cast,handle_info中返回了Timeout | hibernate是这是就会进入相应的处理过程。若返回了Timeout,表示在指定的时间范围内没有接收到请求或消息就会就会发出一条timeout消息,然后进行后续处理;若返回hibernate则表示进程就如hibernate状态,便于GC,当向该进程发送消息或请求的时候, 唤醒该进程然后进行后续处理。
一 Timeout
  1.  1 -module(add).
    2 -behaviour(gen_server).
    3 %% API
    4 -export([start_link/0, add1/2]).
    5 %% gen_server callbacks
    6 -export([init/1,
    7 handle_call/3,
    8 handle_cast/2,
    9 handle_info/2,
    10 terminate/2,
    11 code_change/3]).
    12 -define(SERVER,?MODULE).
    13 -record(state,{}).
    14 start_link()->
    15 gen_server:start_link({local,?SERVER},?MODULE,[],[]).
    16 add1(Num1,Num2)->
    17 io:format("~nsync start~n"),
    18 Res= gen_server:call(?SERVER,{add1,Num1,Num2}),
    19 io:format("~nsync end~n"),
    20 Res.
    21 %%%===================================================================
    22 %%% gen_server callbacks
    23 %%%===================================================================
    24 init([])->
    25 %%erlang:send_after(2000,?SERVER,{add3,1,1}),
    26 %%{ok,#state{}, hibernate}.
    27 {ok,#state{},3000}.
    28 handle_call({add1,Num1,Num2},_From,State)->
    29 Num=Num1+Num2,
    30 timer:sleep(3000),
    31 %%io:format("sleep end~n"),
    32 {reply,{ok, add1,Num},State};
    33 handle_call(_Request,_From,State)->
    34 {reply, ok,State}.
    35 handle_cast(_Request,State)->
    36 {noreply,State}.
    37 handle_info(timeout,State)->
    38 io:format("~ntimeout_ericlw~n"),
    39 {noreply,State};
    40 handle_info({add3,Num1,Num2},State)->
    41 Num=Num1/Num2,
    42 io:format("~n~p~n",[Num]),
    43 timer:sleep(3000),
    44 io:format("sleep end~n"),
    45 {noreply,State};
    46 handle_info(_Info,State)->
    47 {noreply,State}.
    48 terminate(_Reason,_State)->
    49 timer:sleep(3000),
    50 io:format("~nclean up~n"),
    51 ok.
    52 code_change(_OldVsn,State,_Extra)->
    53 {ok,State}.
打印的过时信息:
Gen_server行为分析与实践
若在时限内接受到消息:
Gen_server行为分析与实践
二 hibernate
在返回中用hibernate代替Timeout
  1. 1 init([])->
    2 {ok,#state{}, hibernate}.
后面会用专题来分析该特性,它是重要的优化手段。详细信息:erlang:hibernate和proc_lib:hibernate.

4.总结

Gen_server行为作为通用服务器,良好的将业务部分与通用部分进行了分离,只要专注与业务部分也能够构建良好的系统。我们只需关心导出函数与回调函数部分,明确调用函数与回调函数的意义与联系。gen_server一般是业务模块的核心处理进程,对于请求与消息的处理,应该根据业务来定。对于需要进行非常复杂化的初始化过程,可以通过enter_loop讲一个已经初始化好的进程变成gen_server进程,对于不经常访问的进程记得返回hibernate来进行及时的GC.总之,gen_server为我们构建服务器提供了极大的便利。
 
优秀的代码是艺术品,它需要精雕细琢!