Gen_server行為分析與實踐


1.簡介

Gen_server實現了通用服務器client_server原理,幾個不同的客戶端 分享服務端管理的資源(如圖),gen_server提供標准的接口函數和包含追蹤功能以及錯誤報告來實現通用的服務器,同時可以作為OTP監控樹的一部分。
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}.
調用結果:
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};%%無返回
調用端返回:
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}.
打印的過時信息:
若在時限內接受到消息:
二 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為我們構建服務器提供了極大的便利。
 
優秀的代碼是藝術品,它需要精雕細琢!






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM