[erlang-questions] The gen server simplified (how it works)

Håkan Mattsson hm@REDACTED
Thu Apr 21 16:32:40 CEST 2011


On Wed, Apr 20, 2011 at 10:05 AM, Joe Armstrong <erlang@REDACTED> wrote:
> Many users seem to use the gen_server for absolutely everything
> and often force their problems to fit the gen_sever even though the
> gen_server is not appropriate for their problems.
>
> The gen_server is an extremely simple bit of code which can
> be easily changed to fit other problems, though people don't
> often do this.
>
> In this posting I'll explain the basic idea of how the gen_server works.
>
> To illustrate this I've written mini_gs.erl - this is a mini gen_server
> if you understand mini_gs.erl you'll have understood 98% of how the
> real gen_server works. The real gen_server just adds a load of "bells
> and whistles" to mini_gs.erl.

I tend to use proc_lib (spawn_link, start_link, init_ack) and sys
(handle_system_msg) a lot. They gives you almost all the bells and
whistles that gen_server has, but it enables you to have the same
simple code structure that you have in your very pedagogic code
snippet below.

/Håkan

PS.

  By the way, it seems more appropriate to use spawn_link in a
  function called start_link.

> mini_gs.erl has a compatible interface to gen_server.erl (for a subset of
> the 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.
>
> There. That wasn't so painful. The client starts by calling
>
>    min_gs:start_link({local,Name}, Mod, Args, Opts)
>
> I've ignored Opts in mini_gs, also frozen the name of the server to be of
> the form {local, Name} (gen_server has more general arguments for the
> name of the server)
>
> What happens?
>
> mini_gs calls Mod:init(Args) to initialize the server, this must return
> {ok, State} and State becomes the initial state of the server.
>
> Now mini_gs calls loop(Mod, State.) When mini_gs receives a messag
> {From, Tag, X}
> it calls Mod:handle_call(X, From, State) which returns {repy, R, State1}.
> R1 is sent back to the client, and the server calls loop/2 with the new
> state State1.
>
> That's it. call/2 is an interface routine to abstract out the interface
> between the client and the sever.
>
> Now we can write a simple client application.
>
>   -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}.
>
> This module can call either gen_server or mini_gs (just change the
> define statement)
>
> So now we have turned a single process key-value store (using dict)
> into a global key-value store.  Note that kv.erl never uses the primitives
> spawn_link, send, receive or do on. ie kv.erl is written with pure
> *sequential* code.
>
> This is *why* we made the gen_server abstraction. You can write
> well-typed sequential code (the handle_call and init functions) to
> parametrize
> a concurrent behavior, ie you need to know nothing about concurrency
> to get the job done. We've "abstracted out" the concurrency.
>
> Things become problematic when you do not entirely understand the
> abstraction. Maybe the abstraction is inappropriate for your needs. I
> have seen many examples of code where the gen_server *is* inappropriate.
> The acid test is "does the gen_sever code look like spaghetti" if the
> answer is yes then all you have done is shoe horn the applications into
> an inappropriate form. In this case you should ditch the gen_server and
> roll-your own.
>
> /Joe



More information about the erlang-questions mailing list