1.简介

Gen_server实现了通用服务器client_server原理,几个不同的客户端去分享服务端管理的资源(如图),gen_server提供标准的接口函数和包含追踪功能以及错误报告来实现通用的服务器,同时可以作为OTP监控树的一部分。
Gen_server函数与回调函数之间的关系:
  1.  gen_server moduleCallbackmodule
    --------------------------------
    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
如果回调函数失败或者是返回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/1, whereis_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.  add1(Num1,Num2)->
    io:format("~nsync start~n"),
    Res= gen_server:call(?SERVER,{add1,Num1,Num2}),%%同步请求
    io:format("~nsync end~n"),
    Res.
    handle_call({add1,Num1,Num2},_From,State)->%%消息的接受处理方式
    Num=Num1+Num2,
    timer:sleep(3000),
    io:format("sleep end~n"),
    {reply,{ok, add1,Num},State}.
二 异步请求
通过gen_server:cast或gen_server:abcast发起异步请求立即返回ok|abcast,如果节点|gen_server|Name不存在请求ignore.
  1.  add2(Num1,Num2)->
    io:format("~nasync start~n"),
    Res= gen_server:cast(?SERVER,{add2,Num1,Num2}),%%异步请求
    io:format("~nasync end~n"),
    Res.
    handle_cast({add2,Num1,Num2},State)->%%消息的接受处理方式:
    Num=Num1+Num2,
    io:format("~n~p~n",[Num]),
    timer:sleep(3000),
    io:format("sleep end~n"),
    {noreply,State}.
三 其他消息处理
异步接收发送过来的消息,并进行相应的处理。例如在回调函数中返回{ok,State,Timeout},如果超时就会发送一条timeout消息gen_server(实例3.5).

  1.  add3(Num1,Num2)->
    io:format("~nsend start~n"),
    Res= erlang:send(?SERVER,{add3,Num1,Num2}),%%进程消息发送(异步)
    io:format("~nsend end~n"),
    Res.
    handle_info({add3,Num1,Num2},State)->%%消息的接受处理方式
    Num=Num1+Num2,
    io:format("~n~p~n",[Num]),
    timer:sleep(3000),
    io:format("sleep end~n"),
    {noreply,State}.
3.2 捕获异常消息
通过在gen_server初始化时设置process_flag(trap_exit,true)可以捕获本进程的退出消息。注意:要捕获exit就要进程之间要建立连接。
  1.  exit(Msg)->
    link(whereis(?MODULE)),
    erlang:exit(Msg).
    handle_info({'EXIT',From,Reson},State)->
    io:format("~p~p~n",[From,Reson]),
    {noreply,State};
    handle_info(_Info,State)->
    {noreply,State}.
调用结果:
3.3 gen_server:reply/2
当Modile:handle_call没有返回时{noreply,NewState},可以通过gen_server:reply(Client, Reply)来返回给gen_server:call调用端。
  1.  add10(Num1,Num2)->
    io:format("~nsync start~n"),
    Res= gen_server:call(?SERVER,{noreply,Num1,Num2}),
    io:format("~nsync end~n"),
    Res.
    handle_call({noreply,Num1,Num2},_From,State)->
    Num=Num1+Num2,
    timer:sleep(3000),
    io:format("sleep end~n"),
    gen_server:reply(_From,{ok, gen_server_reply,Num}),%%给gen_server返回Replay
    {noreply,State};%%无返回
调用端返回:
3.4 gen_server:enter_loop/3
gen_server:enter_loop方法可以让一个存在的普通进程成为一个gen_server进程。
  1.  -module(enter).
    -author("EricLw").
    %% API
    -export([start_link/0, init/1]).
    %% API
    -export([ add1/2]).
    %% gen_server callbacks
    -export([
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3]).
    -define(SERVER,?MODULE).
    -record(state,{}).
    %%%===================================================================
    %%% API
    %%%===================================================================
    start_link()->
    proc_lib:start_link(?SERVER, init,[self()]).%%通过proc_link来穿件普通进程
    add1(Num1,Num2)->
    io:format("~nsync start~n"),
    Res= gen_server:call(?SERVER,{add1,Num1,Num2}),
    io:format("~nsync end~n"),
    Res.
    init(Person)->
    proc_lib:init_ack(Person,{ok,self()}),
    register(?MODULE,self()),
    gen_server:enter_loop(?MODULE,[],#state{},{local,?MODULE}). %%指定了ServerName必须先注册Name
    handle_call({add1,Num1,Num2},_From,State)->
    Num=Num1+Num2,
    timer:sleep(3000),
    io:format("sleep end~n"),
    {reply,{ok, add1,Num},State};
    handle_call(_Request,_From,State)->
    {reply, ok,State}. handle_cast(_Request,State)->
    {noreply,State}. handle_info(_Info,State)->
    {noreply,State}.
    terminate(_Reason,_State)->
    timer:sleep(3000),
    io:format("~nclean up~n"),
    ok.
    code_change(_OldVsn,State,_Extra)->
    {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.  -module(add).
    -behaviour(gen_server).
    %% API
    -export([start_link/0, add1/2]).
    %% gen_server callbacks
    -export([init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3]).
    -define(SERVER,?MODULE).
    -record(state,{}).
    start_link()->
    gen_server:start_link({local,?SERVER},?MODULE,[],[]).
    add1(Num1,Num2)->
    io:format("~nsync start~n"),
    Res= gen_server:call(?SERVER,{add1,Num1,Num2}),
    io:format("~nsync end~n"),
    Res.
    %%%===================================================================
    %%% gen_server callbacks
    %%%===================================================================
    init([])->
    %%erlang:send_after(2000,?SERVER,{add3,1,1}),
    %%{ok,#state{}, hibernate}.
    {ok,#state{},3000}.
    handle_call({add1,Num1,Num2},_From,State)->
    Num=Num1+Num2,
    timer:sleep(3000),
    %%io:format("sleep end~n"),
    {reply,{ok, add1,Num},State};
    handle_call(_Request,_From,State)->
    {reply, ok,State}.
    handle_cast(_Request,State)->
    {noreply,State}.
    handle_info(timeout,State)->
    io:format("~ntimeout_ericlw~n"),
    {noreply,State};
    handle_info({add3,Num1,Num2},State)->
    Num=Num1/Num2,
    io:format("~n~p~n",[Num]),
    timer:sleep(3000),
    io:format("sleep end~n"),
    {noreply,State};
    handle_info(_Info,State)->
    {noreply,State}.
    terminate(_Reason,_State)->
    timer:sleep(3000),
    io:format("~nclean up~n"),
    ok.
    code_change(_OldVsn,State,_Extra)->
    {ok,State}.
打印的过时信息:
若在时限内接受到消息:
二 hibernate
在返回中用hibernate代替Timeout

  1.  init([])->
    {ok,#state{}, hibernate}.
后面会用专题来分析该特性,它是重要的优化手段。详细信息:erlang:hibernate和proc_lib:hibernate.

4.总结

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

 
优秀的代码是艺术品,它需要精雕细琢!

Gen_server行为分析与实践的更多相关文章

  1. Log4j2分析与实践

    当前网络上关于Log4j2的中文文章比较零散,这里整理了一下关于Log4j2比较全面的一些文章,供广大技术人员参考 Log4j2分析与实践-认识Log4j2 Log4j2分析与实践-架构 Log4j2 ...

  2. 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用

    https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...

  3. 《Linux内核分析》实践4

    <Linux内核分析> 实践四--ELF文件格式分析 20135211李行之 一.概述 1.ELF全称Executable and Linkable Format,可执行连接格式,ELF格 ...

  4. 自定义View系列教程04--Draw源码分析及其实践

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  5. Supervisor行为分析和实践

    1.简介     Erlang要编写高容错性.稳定性的系统,supervisor就是用来解决这一问题的核心思想.通过建立一颗监控树,来组织进程之间的关系,通过确定重启策略.子进程说明书等参数信息来确定 ...

  6. Gen_event行为分析和实践

    1.简介 Gen_event实现了通用事件处理,通过其提供的标准接口方法以及回调函数,在OTP里面的事件处理模块是由一块通用的事件管理器和任意数量的事件处理器,并且这些事件处理器可以动态的添加和删除. ...

  7. AWVS结果分析与实践-XSS

      今天趁着老师接项目,做了一丢丢实践,以下是一点点感触.     都知道AWVS是神器,可是到我手里就是不灵.拿了它扫了一个URL,结果提示XSS漏洞,实践没反应,只好愉快地享受了过程.来看看.   ...

  8. 基于redis的分布式锁的分析与实践

    ​ 前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于 ...

  9. FastText 分析与实践

    一. 前言 自然语言处理(NLP)是机器学习,人工智能中的一个重要领域.文本表达是 NLP中的基础技术,文本分类则是 NLP 的重要应用.在 2016 年, Facebook Research 开源了 ...

随机推荐

  1. 2016 系统设计第一期 (档案一)MVC ajax 获取json数据

    我在做一张表的增删改查的时候,在编辑的时候,需要获取当前选择行对应的Id,然后并且把选择行的Id的对于的数据取出来,代码如下: 列表a标签绑定: Js代码: url: '/Users/GetUserB ...

  2. 【BZOJ1305】 [CQOI2009]dance跳舞

    Description 一次舞会有n个男孩和n个女孩.每首曲子开始时,所有男孩和女孩恰好配成n对跳交谊舞.每个男孩都不会和同一个女孩跳两首(或更多)舞曲.有一些男孩女孩相互喜欢,而其他相互不喜欢(不会 ...

  3. js 判断对象相等

    文笔不是很好,一直在博客园属于那种只看不说的那种,有次心血来潮,想把自己的一些心得记录下来,我认认真真写了大半个小时,谁知一点保存,会话超时然后我的东西不知道去哪里,当时想死的心都有,写博客也就没那个 ...

  4. js数组反转

    var _li = test.getElementsByTagName("li"), arrayObj = [].slice.apply(_li),//_li用apply调用sli ...

  5. js根据id、pid把数据转为树结构

    //格式化树数据 function toTreeData(data) { var pos = {}; var tree = []; var i = 0; while (data.length != 0 ...

  6. MSMQ(消息队列)续

    在上一篇我简单介绍了MSMQ的相关概念,本篇将以代码说明 Message Message是MSMQ的数据存储单元,我们的用户数据一般也被填充在Message的body当中,因此很重要,让我们来看一看其 ...

  7. 【莫队】bzoj 3781,bzoj 2038,bzoj 3289

    好像又有一个星期没更博客了.. 最近疯狂考试...唯一有点收获的就是学会了莫队这种神奇的算法.. 听起来很难..其实是一个很简单的东西.. 就是在区间处理问题时对于一个待求区间[L',R']通过之前求 ...

  8. 基于局部敏感哈希的协同过滤推荐算法之E^2LSH

    需要代码联系作者,不做义务咨询. 一.算法实现 基于p-stable分布,并以‘哈希技术分类’中的分层法为使用方法,就产生了E2LSH算法. E2LSH中的哈希函数定义如下: 其中,v为d维原始数据, ...

  9. Akka学习——术语和概念

    (大部分为翻译) Concurrency vs. Parallelism 并发 vs 并行   并发并不一定同时运行,比如使用时间片,使得两个任务交替执行.而并行是执两个任务真正的同时执行.     ...

  10. HDU4756+Prim

    题意简单:去掉最小生成树的某一条边并补上一条,求MaxVal 思路:贪心(借鉴Yamidie的思路...) 分别求出最小生成树和次最小生成树,再在这两棵树上求最小生成树 #include<std ...