源码位置位于安装目录的lib/stdlib/src下。
之前在使用gen_server时,由于之前自己实现过一个gen_server,因此对它内部的机制也能知道个七七八八,最近在用erlang的fsm模块,突然想读一读它得源码,这才突然发现erlang的源码内部还是做了很复杂的工作,尤其是有个“阴魂不散”的模块proc_lib。
无论是gen_server也好,gen_fsm也好,实际上在start的时候,都是调用的底层gen模块的start/start_link函数,由start函数调用do_spawn函数,在这里,就和我之前想象的不一样了,在我之前的印象里,这里应该直接用spawn,回调用户编写的init函数,但是,实际上不是这样的,我们看下源码:
Time = timeout(Options),
%这个函数获取timeout的时间,默认为infinity
然后开始调用proc_lib的start函数:
proc_lib:start(?MODULE, init_it, [GenMod, self(),
self(), Name, Mod, Args, Options], Time, spawn_opts(Options))
其中,spawn_opts实在获取spawn的参数。
我们跟进到proc_lib的内部,看看start函数做了些什么。
在start函数的第一行返回了一个Pid,那么我们已经能够猜到spawn_opt内部应该是spawn新的进程了。在spawn_opt函数中,首先获取了Parent(proc_lib进程的名字)以及Ancestors,然后调用erlang模块的spawn_opt函数。
这个函数会调用当前进程的init_p方法,现在要注意,此时spawn_opt派生了新的进程,那么原来的parent进程会直接返回一个Pid,这个Pid就被我们之前看到的proc_lib:start的地方接收了,并且继续向下执行了sync_wait(Pid,
Timeout)函数,在sync_wait函数的内部在等待接收消息,如果超过timout的时间没有接收到,就认为失败了,那么我们可以推测,派生出来的进程一定是去执行用户的init的函数去了,并且在执行完后会发给parent进程一个消息。
我们看看我们的猜测对不对,继续跟进到init_p函数内部,我们可以看到函数内部开始搜刮者用户传递进来的Fun的所有信息,并放入进程字典中(真的不喜欢进程字典),然后开始执行用户的Fun函数,然后该进程结束。
是的,你没有看错,你没有发现任何地方向parent进程发送了ack信号,这不科学,因为这意味着parent会因为超时而报错,如果你这么想,你一定是把执行的Fun函数当成了你的init,我们网上看就能发现实际上是gen模块的init_it函数,这里还有个不太好的地方就是init_it调用了函数init_it2…这命名规则着实让人蛋疼……
init_it2回调了用户传入的init_it函数,这个函数是由对应的行为模式编写的init_it函数,比如举个简单的例子,在gen_server的源码中,init_it就调用了用户传入的init函数,并且在执行后,调用init_ack,init_ack函数向parent进程发送了一个成功的消息,之后,gen_server进入loop函数开始接收消息,也就是说,用户的gen_server:start()函数执行完毕。也印证了我们之前的猜想是正确的。
其实,这里我有个疑问,不知道作者为什么不把proc_lib:init_ack函数放在proc_lib的init_p函数的最后执行呢?
我们只是简单的分析了proc_lib的spawn与直接spawn有哪些不同,那么对于我们来说,proc_lib还有哪些直接挖掘的呢?为什么要用proc_lib去包装spawn呢?
翻翻proc_lib的代码,在上文中,我们也可以看出,在spawn之前,保存了不少信息,比如parent进程的信息和相关的函数信息,这些在用procss_info查看时,都可以直观的看到。同时,模块向外暴露的initial_call函数,translate_initial_call函数以及raw_initial_call函数也可以查看。至于函数内部的实现就不多讨论了,比较简单。
最后,我们会发现还有个有趣的函数crash_report,我们分析源码能够看出,对于proc_lib派生的进程来说,退出信号是normal以及shutdown都会被认为是正常退出。其他的出错信息会被error_logger模块捕捉。
最后的最后,还有一个非常有意思的函数,就是hibernate函数,这个函数看起来就让java程序员很亲切,官方手册上,对erlang模块的hibernate函数的解释大约是把当前正在运行的线程处于一个wait的状态,此时,进程会抛弃掉自己的调用栈,并且进行gc,然后wait,直到信箱中接受到了新的消息。很明显的,这种情况应该是当前进程在一段时间内预计不会收到消息,为了节省内存而触发的,在高并发的场景下对内存的节省会起到一定的作用。同时,文档上还说,当进程收到新的消息被唤醒,并且将函数的控制权交给作为参数被传入的Fun。
问题出现了,既然调用栈被抛弃了,那么Fun执行完后何去何从?显然进程这不就直接结束了?当然不是,proc_lib对hibernate做了个封装,当被唤醒时会回调用户传入的函数,比如gen_server,那么wake_hib最终会被回调,我们可以看到,在gen_server中,wake_hib在处理完刚刚接受到得消息后,重新回到了handle_msg的函数中继续等待接收消息。
对于proc_lib,值得说说的也就这么多,代码不是很长,大约700多行,这个模块理解了,本身gen模块东西也不多,那么所有的otp模式也就比较简单了。
恩,就是这样。