Joe Armstrong:
许多用户常常使用gen_server
似乎认为它能够解决所有事情,并且尝试强制他们的问题适用于gen_server
,尽管gen_server
不适合他们的问题。
gen_server
是一个非常简单的代码,可以很容易的改变它,以适用于其他问题,然而人们并不常常这样做。
在这篇文章中,我将介绍gen_server
的工作原理。
为了说明这一点,我写了mini_gs.erl
,如果你能明白mini_gs
,这是一个迷你gen_server
。你会明白98%的gen_server
真实工作情况,真正的gen_server
只是在mini_gs
的基础上添加了一些华而不实的东西。
mini_gs.erl
具有与gen_server.erl
的兼容接口(gen_server
API的一个子集)
-module(mini_gs).
-export([start_link/4, call/2]).
%% this module behaves just like the gen-server for a sub-set of the gen_server
%% commands
start_link({local,Name}, Mod, Args, _Opts) ->
register(Name, spawn(fun() -> start(Mod, Args) end)).
call(Name, X) ->
Name ! {self(), Ref = make_ref(), X},
receive
{Ref, Reply} -> Reply
end.
start(Mod, Args) ->
{ok, State} = Mod:init(Args),
loop(Mod, State).
loop(Mod, State) ->
receive
{From, Tag, X} ->
case Mod:handle_call(X, From, State) of
{reply, R, State1} ->
From ! {Tag, R},
loop(Mod, State1)
end
end.
这里没有那么麻烦,客户端通过调用mini_gs:start_link({local,Name},Mod,Args,Opts)
开始
我忽略了mini_gs
中的选项,也固定了server的名称格式{local,Name}
(gen_server
对于server的名称有更多的一般参数)
接下来什么发生了
mini_gs
调用了Mod:init(Args)
用来初始化server,这要求返回{ok,State}
,
并且State成为服务的初始状态。
现在mini_gs
调用 loop(Mod,State)
。当mini_gs
收到{From,Tag,X}
消息,会调用Mod:handle_call(X,From,State ),
这要求返回{reply,R,State1}
。R1会返回给客户端,服务器以新的状态State1
调用loop/2
。
仅此而已,call/2
是一个接口程序,用来抽象出客户端和服务端之间的接口。
现在我们可以编写一个简单的客户端应用。
-module(kv).
%% These define the client API
-export([start/0, store/2,lookup/1]).
%% these must be defined because they are called by gs
-export([init/1, handle_call/3]).
-define(GS, mini_gs).
%% -define(GS, gen_server).
%% define the client API
start() -> ?GS:start_link({local,someatom}, kv, foo, []).
store(Key,Val) -> ?GS:call(someatom, {putval,Key,Val}).
lookup(Key) -> ?GS:call(someatom, {getval,Key}).
%% define the internal routines
init(foo) -> {ok, dict:new()}.
handle_call({putval, Key, Val}, _From, Dict) ->
{reply, ok, dict:store(Key, Val, Dict)};
handle_call({getval,Key}, _From, Dict) ->
{reply, dict:find(Key, Dict), Dict}.
该模块可以调用gen_server
或mini_gs
(只需要修改define声明)
所以现在我们已经把一个单一键值存储进程(使用dict)转换成了全局key-value
存储。请注意kv.erl
并没有使用原语spawn_link
发送、接收、或在此基础上。即kv.erl
是用纯序列代码编写的。
这是为什么我们做了gen_server
抽象。你可以编写良好的顺序代码(handle_call
和init
函数)进行参数化并发行为,即你完成工作却并不需要了解并发性。我们已经"抽象出"了并发性。
当你完全不理解抽象部分的时候,事情变得有问题。也许抽象的不适合你的需要。我曾经看到很多这样的例子,其中gen_server
是不合适的。关键性的测试使gen_server
代码看起来显示格式意大利面条一样杂乱无序,如果是这样的,那么所有你做的是使应用成为不适当的形式,像不合适的鞋码一样蹩脚,你应该挖掘gen_server
定制为你自己的。
参考:Joe amstrong的mail
链接:找回自己