相信用过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不仅让你写出高效的服务,而且还会避免不必要的麻烦。