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

`Erlang` `gen_server ` --- ## 直接上代码 ``` erlang -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 行为 ``` erlang -module(study). -behaviour(gen_server). ``` 然后是类似要实现对应的协议?接口?的感觉 (Erlang 里不写 export 的方法都是私有方法。) ``` erlang -export([init/1, handle_call/3, handle_cast/2, terminate/2]) ``` 上面这几个是 gen_server 需要的几个函数。接下来就是实现这些函数了。 首先先过一下运行流程。 --- ## 基本运行流程 ### 第零步,编译 代码保存为 study.erl 注意文件名要和模块名一致。 然后 `erl` 进入控制台,cd 到源文件所在目录,执行 `c(study).` 对源文件进行编译。 ### 第一步,初始化 执行 `study:start_link().` 这个没什么说的,肯定会执行下面这段代码 ``` erlang 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`,也就是 ``` erlang init(_Args) -> {ok, channels()}. ``` 里面又调用了 `channels/0` 也就是 ``` erlang 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().` 毋庸置疑肯定执行下面这段代码 ``` erlang alloc() -> gen_server:call(my_study, alloc). ``` 所以 `gen_server:call(my_study, alloc).` 这句又是做什么的呢。其实就是调用注册名为 `my_study` 对应的 `alloc` 函数?不对,由于没有指定函数参数个数,Erlang 不可能知道去调哪个函数。 其实,这里,调用(回调?)的是注册名为 `my_study` 对应的 `handle_call/3`。 ``` erlang 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` 都做了点啥。 ``` erlang 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).` 的时候首先会执行 ``` erlang free(Ch) -> gen_server:cast(my_study, {free, Ch}). % 注意这里是 gen_server:cast 不是 gen_server:call ``` 然后执行的是 `handle_cast/2` ``` erlang 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().` 函数 ``` erlang stop() -> gen_server:cast(my_study, stop). ``` 分析过前几步的例子,这里就比较清晰了,它会触发 ``` erlang handle_cast(stop, State) -> {stop, normal, State}. ``` 注意到这里并没有显式的调用 `terminate/2`,是由 gen_server 负责调用,做最后的处理工作,处理完毕就会退出这个进程了。 再次通过 init 启动后,之前维护的值就自然也跟着不见了。反之如果你不终止就开启一个同名的服务,那肯定是会报错的。 ## 结语 以上是黑盒分析的结果,其实实现一个简化版的 `gen_server` 只需几行代码。参见「坚强哥」的博文[理解Erlang/OTP gen_server](http://www.cnblogs.com/me-sa/archive/2011/12/20/erlang0023.html) 拆开来看能更深的理解背后的原理。 `gen_server` 还有许多功能,比如热更新,与 `Supervisor` 配合使用等。下回慢慢分析。 --- ### 参考链接 [理解Erlang/OTP gen server ](http://www.cnblogs.com/me-sa/archive/2011/12/20/erlang0023.html) [OTP Design Principles User's Guide Chapters 2 gen server Behaviour](http://erlang.org/doc/design_principles/gen_server_concepts.html) [[Erlang 学习笔记]erlang behaviour小结之gen_server](http://blog.csdn.net/lqg1122/article/details/7484413)