Erlang
gen_server
直接上代码
-module(study).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).
-export([start_link/0]).
-export([alloc/0,free/1]).
-export([stop/0]).
start_link() ->
gen_server:start_link({local, my_study}, study, [], []).
init(_Args) ->
{ok, channels()}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
alloc() ->
gen_server:call(my_study, alloc).
handle_call(_Request, _From, State) ->
io:format("取出之前的状态 ~w~n", [State]),
{Ch, State2} = alloc(State),
io:format("取出的数字 ~w~n", [Ch]),
io:format("取出之后的状态 ~w~n", [State2]),
{reply, Ch, State2}.
free(Ch) ->
gen_server:cast(my_study, {free, Ch}).
stop() ->
gen_server:cast(my_study, stop).
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2};
handle_cast(stop, State) ->
{stop, normal, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
terminate(normal, State) ->
io:format("停止时的状态 ~w~n", [State]),
ok.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
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.
前两句是声明模块名和引入 gen_server 行为
-module(study).
-behaviour(gen_server).
然后是类似要实现对应的协议?接口?的感觉
(Erlang 里不写 export 的方法都是私有方法。)
-export([init/1, handle_call/3, handle_cast/2, terminate/2])
上面这几个是 gen_server 需要的几个函数。接下来就是实现这些函数了。
首先先过一下运行流程。
基本运行流程
第零步,编译
代码保存为 study.erl 注意文件名要和模块名一致。
然后 erl
进入控制台,cd 到源文件所在目录,执行 c(study).
对源文件进行编译。
第一步,初始化
执行 study:start_link().
这个没什么说的,肯定会执行下面这段代码
start_link() ->
gen_server:start_link({local, my_study}, study, [], []).
再观察函数里面的情况,执行了 gen_server:start_link/4
是干什么的呢。
这个例子中,第一个参数是个元组,表示要在 local 本地注册一个名叫 my_study 的 server。
第二个参数就是模块名了。
第三个参数是要传给 init
函数的参数,所以这里可以推测出,执行了 gen_server:start_link/4
之后它就会去执行 study:init/1
,也就是
init(_Args) ->
{ok, channels()}.
里面又调用了 channels/0
也就是
channels() ->
{_Allocated = [], _Free = lists:seq(1,100)}. % 初始状态
到这里就停了。只声明了两个变量 _Allocated
和 _Free
。
其实不然。
init(_Args)
如果返回了 {ok, SomeState}
,那么 SomeState
这个变量就会被维护保存起来。(gen_server 的具体实现中,应该是在尾递归循环的参数中保存,专有名词叫 Continuation。)
如果不 ok,那初始化就会出错。
所以这里 {_Allocated = [], _Free = lists:seq(1,100)}
这个元组就被保存起来,之后怎么存取它我们往下看。
第二步,执行 alloc
erl 中输入 study:alloc().
毋庸置疑肯定执行下面这段代码
alloc() ->
gen_server:call(my_study, alloc).
所以 gen_server:call(my_study, alloc).
这句又是做什么的呢。其实就是调用注册名为 my_study
对应的 alloc
函数?不对,由于没有指定函数参数个数,Erlang 不可能知道去调哪个函数。
其实,这里,调用(回调?)的是注册名为 my_study
对应的 handle_call/3
。
handle_call(_Request, _From, State) ->
io:format("~w ~w~n", [_Request, _From]),
io:format("取出之前的状态 ~w~n", [State]),
{Ch, State2} = alloc(State),
io:format("取出的数字 ~w~n", [Ch]),
io:format("取出之后的状态 ~w~n", [State2]),
{reply, Ch, State2}.
handle_call/3
第一个参数接收的就是 gen_server:call(my_study, alloc).
里第二个参数的值。也就是 alloc
这个原子。
第二个参数是调用方的信息,比如 {<0.64.0>, #Ref<0.3946304990.3179544577.15636>}
第三个参数,就是我们上面第一步中最后提到的那个被保存起来的元组!
拿到这个值之后,我们就可以进行真正的操作了,也就是执行 {Ch, State2} = alloc(State),
先不看具体的执行逻辑,最后 handle_call/3
返回了 {reply, Ch, State2}
,那这个是什么意思呢?
我的理解就是 reply 表示可以携带一个返回值出去,返回值内容就是元组的第二个(0 基的话就是第一个)元素的值,第三个就是要更新的『server 维护的那个 state 的新值』
所以最终,维护的内容就变成了 State2
。
再回头看看我们的 alloc/1
和 free/2
都做了点啥。
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.
不难看出 alloc/1
大概就是从 1 到 100 的数字中取出一个数,注意这个 _Free
就是我们初始化的那个列表。
而 free/2
就是把取出的数再放回去。
所以总的来说这模拟了一个申请资源和释放资源的动作流程。
第三步,执行 free
第二步的最后我们已经分析了 free/2
的代码,和 alloc 类似,当我们调用 study:free(1).
的时候首先会执行
free(Ch) ->
gen_server:cast(my_study, {free, Ch}). % 注意这里是 gen_server:cast 不是 gen_server:call
然后执行的是 handle_cast/2
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2};
所以最终是调用了 free/2
,并使用 {noreply, Chs2}
对 server 维护的状态进行更新。
noreply 和 reply 的区别就是 noreply 没有返回值了,最后一个元素依然是要更新的值。
所以通过调用 alloc 和 free 就可以进行申请和释放的动作了。
第四步,stop
为了让这个 server 停下来,如果你把它加入了 Supervisor
中,那就由 Supervisor
来管理了。
如果是像本例中单独启动的情况,可以通过实现 terminate/2
来解决停止的问题。
执行 study:stop().
函数
stop() ->
gen_server:cast(my_study, stop).
分析过前几步的例子,这里就比较清晰了,它会触发
handle_cast(stop, State) ->
{stop, normal, State}.
注意到这里并没有显式的调用 terminate/2
,是由 gen_server 负责调用,做最后的处理工作,处理完毕就会退出这个进程了。
再次通过 init 启动后,之前维护的值就自然也跟着不见了。反之如果你不终止就开启一个同名的服务,那肯定是会报错的。
结语
以上是黑盒分析的结果,其实实现一个简化版的 gen_server
只需几行代码。参见「坚强哥」的博文理解Erlang/OTP gen_server
拆开来看能更深的理解背后的原理。
gen_server
还有许多功能,比如热更新,与 Supervisor
配合使用等。下回慢慢分析。
参考链接
理解Erlang/OTP gen_server
OTP Design Principles User's Guide Chapters 2 gen_server Behaviour
[Erlang 学习笔记]erlang behaviour小结之gen_server