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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容