erlang最常见的组件gen_server

相信用过erlang的同学对gen_server并不陌生,我们在日常使用中,和gen_server打交道的次数也是最多的。至于用法我这边也不会详细的说明,只是略微带过,我会将篇幅用在更加不常用但是却很有用的功能上。

1.一般用法以及原理

call(Name,Request,Timeout)实际是向目标proc发送了{'$gen_call',{self(), Mref}, Request}}的消息

cast(Name,Request)实际是向目标proc发送了{'$gen_cast',{self(), Mref}, Request}}的消息

这些只不过是gen做的更通用语法糖而已call(Process, Label, Request)

在处理消息的时候,并没有先后顺序,而是从message box依次取出消息进行处理,遇到{'$gen_call',{self(), Mref}, Request}}这类消息,丢给回调模块的handle_call处理,遇到{'$gen_cast',{self(), Mref}, Request}}这类消息丢给回调模块的handle_cast处理,剩下的不能识别的消息交给回调模块的handle_info处理,当然这里有一个前提,不是系统消息,比如suspend,resume等,这类消息是另外的处理逻辑,这里代码我就不贴了。

2.合理的使用timeout

2.1 使用timeout可以做超时工作(timer,TTL)

使用场景,例如timer.erl的定时任务
handle_call的返回值中返回{reply,Reply,NewState,Timeout}可以在Timeout时间内收不到消息的情况下,自己主动收到一个timeout的消息,timer就是根据这一原理打造的,从而触发想要执行的MFA
但是同时有一个隐藏的问题存在,那就是系统消息,在处理系统消息之后,它并没有修改超时时间,导致收到timeout消息滞后,举个例子,如何来让timer出现上述问题。之前项目组使用的第三方crontab插件(https://github.com/b3rnie/crontab)也会有这个问题。如果要克服这个问题,不能以gen_server为组件。

Eshell V10.2  (abort with ^G)
1> G = fun()->
1>   F = fun() -> receive Msg -> io:format("~p rev ~p ~n", [erlang:localtime(), Msg]) end end,
1>   P = erlang:spawn(F),
1>   timer:start(),
1>   io:format("now ~p ~n",[erlang:localtime()]),
1>   timer:send_after(1000 * 5, P, {msg, erlang:localtime()}),
1>   sys:suspend(timer_server),
1>   receive
1>   after 1000 * 7 -> ok
1>   end,
1>   sys:resume(timer_server) end.
#Fun
2> G().
now {{2019,11,21},{23,5,25}} 
ok
% 按道理会打印出 {{2019,11,21},{23,5,30}} rev ....,但是由于timer_server被挂起了,所以延后了。
{{2019,11,21},{23,5,37}} rev {msg,{{2019,11,21},{23,5,25}}} 
3> 

源代码分析:

decode_msg(Msg, Parent, Name, State, Mod, Time, HibernateAfterTimeout, Debug, Hib) ->
    case Msg of
    {system, From, Req} ->
        sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug,
        % 问题出在这里!!!这里的Time应该减去操作用的时间
        [Name, State, Mod, Time, HibernateAfterTimeout], Hib);
    {'EXIT', Parent, Reason} ->
        terminate(Reason, ?STACKTRACE(), Name, undefined, Msg, Mod, State, Debug);
    _Msg when Debug =:= [] ->
        handle_msg(Msg, Parent, Name, State, Mod, HibernateAfterTimeout);
    _Msg ->
        Debug1 = sys:handle_debug(Debug, fun print_event/3,
                      Name, {in, Msg}),
        handle_msg(Msg, Parent, Name, State, Mod, HibernateAfterTimeout, Debug1)
    end.
2.2 hibernate可以优化内存

如果可以预见在短时间内没有消息到达,可以让gen_server休眠(hibernate),可以大大的节省内存开支,为此我专门写了一个测试代码(https://github.com/aijingsun6/erl_hibernate.git),两种情况都生成10万个gen_server,一种情况使用hibernate,另一种情况不使用hibernate,测试结果如下:

% 使用hibernate
rebar.config
{erl_opts, [
  {d, hibernate},
  debug_info
]}.

rebar compile
werl -pa ebin -P 300000 -s erl_hibernate
1> erlang:memory().

[{total,209592416},
 {processes,170086224},
 {processes_used,170085280},
 {system,18446744073749057808},
 {atom,3699033},
 {atom_used,3694166},
 {binary,692816},
 {code,4682355},
 {ets,351424}]
 
 % 不使用hibernate
 rebar.config
{erl_opts, [
  %{d, hibernate},
  debug_info
]}.

rebar compile
werl -pa ebin -P 300000 -s erl_hibernate
1> erlang:memory().

[{total,369103040},
 {processes,330034704},
 {processes_used,330033760},
 {system,39068336},
 {atom,3404049},
 {atom_used,3393117},
 {binary,624272},
 {code,4615561},
 {ets,350464}]

可见在使用hibernate的情况下,内存大幅减少

3. debug大有用途

trace 可以打印每一条Msg
log 可以捕获倒数N条Msg
log_to_file 可以将Msg输出到文本,特别适用线上追溯问题
statistics 可以统计一段时间的reductions,消息的进(in)出(out)数量

总结

正确理解gen_server不仅让你写出高效的服务,而且还会避免不必要的麻烦。

你可能感兴趣的:(erlang最常见的组件gen_server)