Gen_server行为分析与实践

1.简介

Gen_server实现了通用服务器client_server原理,几个不同的客户端去分享服务端管理的资源(如图),gen_server提供标准的接口函数和包含追踪功能以及错误报告来实现通用的服务器,同时可以作为OTP监控树的一部分。

Gen_server函数与回调函数之间的关系:

 1 gen_server module Callback module
 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/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)->
 2
 3 io:format("~nsync start~n"),
 4 Res = gen_server:call(?SERVER, {add1, Num1, Num2}),%%同步请求
 5 io:format("~nsync end~n"),
 6 Res.
 7 handle_call({add1, Num1, Num2}, _From, State) -> %%消息的接受处理方式
 8 Num = Num1 + Num2,
 9 timer:sleep(3000),
10 io:format("sleep end~n"),
11 {reply, {ok, add1, Num}, State}.

二 异步请求

通过gen_server:cast或gen_server:abcast发起异步请求立即返回ok|abcast,如果节点|gen_server|Name不存在请求ignore.

 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 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 exit(Msg) ->
2 link(whereis(?MODULE)),
3 erlang:exit(Msg).
4
5 handle_info({‘EXIT‘, From, Reson}, State) ->
6 io:format("~p~p~n",[From, Reson]),
7 {noreply, State};
8 handle_info(_Info, State) ->
9 {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)->
 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}; %%无返回

调用端返回:

3.4 gen_server:enter_loop/3

gen_server:enter_loop方法可以让一个存在的普通进程成为一个gen_server进程。

 1 -module(enter).
 2 -author("EricLw").
 3
 4
 5 %% API
 6 -export([start_link/0, init/1]).
 7 %% API
 8 -export([ add1/2]).
 9
10 %% gen_server callbacks
11 -export([
12 handle_call/3,
13 handle_cast/2,
14 handle_info/2,
15 terminate/2,
16 code_change/3]).
17
18 -define(SERVER, ?MODULE).
19
20 -record(state, {}).
21
22 %%%===================================================================
23 %%% API
24 %%%===================================================================
25
26 start_link() ->
27 proc_lib:start_link(?SERVER, init, [self()]). %%通过proc_link来穿件普通进程
28
29 add1(Num1, Num2)->
30 io:format("~nsync start~n"),
31 Res = gen_server:call(?SERVER, {add1, Num1, Num2}),
32 io:format("~nsync end~n"),
33 Res.
34
35 init(Person) ->
36 proc_lib:init_ack(Person, {ok, self()}),
37 register(?MODULE, self()),
38 gen_server:enter_loop(?MODULE, [], #state{},{local,?MODULE}). %%指定了ServerName必须先注册Name
39
40 handle_call({add1, Num1, Num2}, _From, State) ->
41 Num = Num1 + Num2,
42 timer:sleep(3000),
43 io:format("sleep end~n"),
44 {reply, {ok, add1, Num}, State};
45 handle_call(_Request, _From, State) ->
46 {reply, ok, State}.
47
48
49 handle_cast(_Request, State) ->
50 {noreply, State}.
51
52
53 handle_info(_Info, State) ->
54 {noreply, State}.
55
56 terminate(_Reason, _State) ->
57 timer:sleep(3000),
58 io:format("~nclean up~n"),
59 ok.
60
61 code_change(_OldVsn, State, _Extra) ->
62 {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).
 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
15 start_link() ->
16 gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
17 add1(Num1, Num2)->
18 io:format("~nsync start~n"),
19 Res = gen_server:call(?SERVER, {add1, Num1, Num2}),
20 io:format("~nsync end~n"),
21 Res.
22 %%%===================================================================
23 %%% gen_server callbacks
24 %%%===================================================================
25 init([]) ->
26 %%erlang:send_after(2000,?SERVER,{add3, 1, 1}),
27 %%{ok, #state{}, hibernate}.
28 {ok, #state{},3000}.
29 handle_call({add1, Num1, Num2}, _From, State) ->
30 Num = Num1 + Num2,
31 timer:sleep(3000),
32 %%io:format("sleep end~n"),
33 {reply, {ok, add1, Num}, State};
34 handle_call(_Request, _From, State) ->
35 {reply, ok, State}.
36
37 handle_cast(_Request, State) ->
38 {noreply, State}.
39
40 handle_info(timeout, State) ->
41 io:format("~ntimeout_ericlw~n"),
42 {noreply, State};
43 handle_info({add3, Num1, Num2}, State) ->
44 Num = Num1 / Num2,
45 io:format("~n~p~n", [Num]),
46 timer:sleep(3000),
47 io:format("sleep end~n"),
48 {noreply, State};
49 handle_info(_Info, State) ->
50 {noreply, State}.
51
52 terminate(_Reason, _State) ->
53 timer:sleep(3000),
54 io:format("~nclean up~n"),
55 ok.
56
57 code_change(_OldVsn, State, _Extra) ->
58 {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为我们构建服务器提供了极大的便利。

博客地址:http://www.cnblogs.com/liuweiccy/p/4641702.html

源码地址:https://github.com/liuweiccy/test_sup_multi/tree/master/test_sup_gen_server

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

时间: 07-11

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

Log4j2分析与实践

当前网络上关于Log4j2的中文文章比较零散,这里整理了一下关于Log4j2比较全面的一些文章,供广大技术人员参考 Log4j2分析与实践-认识Log4j2 Log4j2分析与实践-架构 Log4j2分析与实践-配置示例 Log4j2分析与实践-配置详解 Log4j2分析与实践-Lookups Log4j2分析与实践-Appenders Log4j2分析与实践-Layouts Log4j2分析与实践-Filters Log4j2分析与实践-异步Logger Log4j2分析与实践-无垃圾地写日志

Supervisor行为分析和实践

1.简介 Erlang要编写高容错性.稳定性的系统,supervisor就是用来解决这一问题的核心思想.通过建立一颗监控树,来组织进程之间的关系,通过确定重启策略.子进程说明书等参数信息来确定佣程与督程的行为,以及在发生故障时的处理办法.简单介绍supervisor的API:      start_link(Module, Args) -> startlink_ret()      start_link(SupName, Module, Args) -> startlink_ret() 用来启

Gen_event行为分析和实践

1.简介 Gen_event实现了通用事件处理,通过其提供的标准接口方法以及回调函数,在OTP里面的事件处理模块是由一块通用的事件管理器和任意数量的事件处理器,并且这些事件处理器可以动态的添加和删除.一个事件可以用来记录error,alarm,info, warning等信息.一个事件管理器可以安装0,1,N个事件处理器,当一个事件管理器接受到一个事件的通知时,这个事件将会被所有的已安装的事件处理器处理(如图). 事件管理器实质上是{Module, State}组成的列表,每个Module是一个

[测试技术思考] 软件可测性分析和实践

软件测试中可测性一般是指对系统的可控性.可观测性进行的评估,借以反映系统设计.实现对测试的友好程度和相应的测试成本.可测性在测试阶段会对系统的测试成本及关联产品代码的Patch次数产生重大影响.如何提高可测性成为软件生命周期特别是前期(设计阶段.coding阶段)重要的一环. 本文带领大家探索在实际项目中可测性相关的实战经验和对应的改进措施. 1 提高可测性的切入点 可测性的评估和改进最早开始于两个阶段: a. 新项目的设计阶段: b. 已有项目新功能.新策略的提测阶段. 这些是提高团队设计的系

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

转:https://my.oschina.net/wnjustdoit/blog/1606215 前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞. 那么,本文主要来讨论基于redis的分布式锁算法问题. 从2.6.12版本开始,redis为SET命令增加了一系列选项(SET key value [EX seconds] [PX

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

通过之前的详细分析,我们知道:在measure中测量了View的大小,在layout阶段确定了View的位置. 完成这两步之后就进入到了我们相对熟悉的draw阶段,在该阶段真正地开始对视图进行绘制. 按照之前的惯例,我们来瞅瞅View中draw( )的源码 public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFL

手动SQL注入原理分析与实践

代码仓库 本文所用代码的代码库地址: 点击这里前往Github仓库 了解SQL注入 定义 SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞.可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限. 原理 造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查

Gearman介绍、原理分析、实践改进

gearman是什么? 它是分布式的程序调用框架,可完成跨语言的相互调用,适合在后台运行工作任务.最初是2005年perl版本,2008年发布C/C++版本.目前大部分源码都是(Gearmand服务job Server)C++,各个API实现有各种语言的版本.PHP的Client API与Worker API实现为C扩展,在PHP官方网站有此扩展的中英文文档. gearman架构中的三个角色 client:请求的发起者,工作任务的需求方(可以是C.PHP.Java.Perl.Mysql udf等

分享一次失败的项目实践经验

一.开篇 最近在网上看到了一款canvas实现网页涂鸦效果的作品,感觉这个效果比较奇特而且在以前没有学习canvas这样的功能是不可思议的,所以本人秉着程序员的那一份执着,花了两三个小时的时间来研究了一下canvas涂鸦作品的代码,发现里面代码比较精辟,但是美中不足的是有些代码的结构会比较的混乱,让人感觉层次上面有点不太分明.所以本人就打算对这个代码结构进行重构使其更具有可读性.但是理想是丰满的现实是骨感的,经过这一次代码的分析和实践下来,对我也是打击蛮大的,以前从来都是认为前端只要能够熟悉调试