首页 > 代码库 > Erlang点滴--杀死gen_server
Erlang点滴--杀死gen_server
前天同事碰到了一个问题:他为游戏写了一个模拟客户端的机器人程序,用的是gen_server行为。但是他启动这些机器人时并没有通过监控树,而是直接在Shell下启动了若干个。然后他就发现如果其中一个机器人进程挂掉的话,所有的机器人都会跟着挂掉。
当他把问题告诉我时我第一反应就是Shell挂掉了,因为所有的机器人都是在Shell下用start_link启动的,也就是说所有的机器人进程都和Shell进程建立了连接。因此如果其中一个机器人挂掉,它会向Shell发送EXIT消息导致Shell挂掉,Shell挂掉后又会向与它连接的所有机器人发送EXIT消息,这样就导致所有的机器人都挂掉了。
但是同事说他的每一个机器人启动时都调用了process_flag(trap_exit, true)的,为什么会挂掉呢。
我们知道在Erlang里两个进程如果建立了连接,如果其中一个挂掉,那么它会向另一个发送{‘EXIT’,Pid,Reason}消息。其中这个Pid就是自己的进程ID,Reason如果是normal,那么另一个进程将会忽略该消息,否则另一个进程也将会挂掉自己。注意这些消息的传递是底层实现而不能通过receive语句接收的,如果想要进程可以接收这些语句的话可以使用之前说的process_flag(trap_exit, true)来修改进程属性。这样做的结果是别的进程挂掉的消息将转化为该进程可以接收的消息而放入消息队列(这里面有一个例外,那就是如果Reason=kill的话,该消息依旧不可捕捉,进程将会强制挂掉)。所以这里可以看出调用erlang:exit(Pid, Reason)和Pid ! Reason的效果是不一样的,后者并不能真正挂掉一个进程,即使Reason不是normal。
有点儿跑题了。对于同事的问题我没想通的是,既然每一个机器人都设置了可以捕获退出消息,那么当Shell挂掉的时候,其它机器人按理说应该可以捕获到这个退出消息放入消息队列,怎么也会跟着挂掉呢?看起来好像就是process_flag(trap_exit, true)没有起作用的样子。
实在想不通于是去看了gen_server的源码,在gen_server.erl里可以发现这样一段代码:
%%% ---------------------------------------------------%%% The MAIN loop.%%% ---------------------------------------------------loop(Parent, Name, State, Mod, hibernate, Debug) -> proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, Debug]);loop(Parent, Name, State, Mod, Time, Debug) -> Msg = receive Input -> Input after Time -> timeout end, decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, false).wake_hib(Parent, Name, State, Mod, Debug) -> Msg = receive Input -> Input end, decode_msg(Msg, Parent, Name, State, Mod, hibernate, Debug, true).decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, Hib) -> case Msg of {system, From, Req} -> sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug, [Name, State, Mod, Time], Hib); {‘EXIT‘, Parent, Reason} -> terminate(Reason, Name, Msg, Mod, State, Debug); _Msg when Debug =:= [] -> handle_msg(Msg, Parent, Name, State, Mod); _Msg -> Debug1 = sys:handle_debug(Debug, fun print_event/3, Name, {in, Msg}), handle_msg(Msg, Parent, Name, State, Mod, Debug1) end.
原来gen_server的实现中一直维护了一个父进程,如果退出消息是父进程发过来的,不管Reason是什么,gen_server都会挂掉自己。这也应该就是同事的所有机器人挂掉的原因了,因为Shell就是所有机器人的父进程。
当然同事启动gen_server的方式是不对的,应该用监控树的方式启动。看到这儿也想到了使用gen_server的话一定要设置进程可捕获退出信息,因为如果父进程挂掉了,那么理应让gen_server也跟着挂掉。但如果gen_server没有设置process_flag(trap_exit, true),那么它自己的存活就不完全受其父进程控制,譬如父进程如果是正常挂掉了,gen_server收到一个{‘EXIT’,Parent,normal}的消息(但并不会进消息队列)然后忽略掉,自己依然安然无恙地存活着,这是不应该的。
最后总结出的就是:gen_server一定要利用监控树来启动,也需要设置process_flag(trap_exit, true),否则就没有发挥gen_server本身的价值。
Erlang点滴--杀死gen_server