×

Erlang 学习笔记/1 简单尝试 gen_server

96
BlindingDark
2017.10.17 18:05* 字数 1366

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/1free/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

Erlang
Web note ad 1