您的位置:首页 > 其它

Erlang源码阅读笔记之gen_server进程启动

2017-12-15 00:00 232 查看

概述

gen_server大概是用的最为广泛的OTP组件了,它提供了通用服务进程的生命周期骨架(进程启动、事件循环、消息处理以及终止)和协作API,并通过behaviour规定了各个周期回调函数的形态,只要我们按照gen_server behaviour实现回调函数,就能将其纳入到OTP的服务管理体系当中。

gen_server文档中API与各回调函数的对应关系:

gen_server module apicallback function
gen_server:startModule:init/1
gen_server:start_linkModule:init/1
gen_server:stopModule:terminate/2
gen_server:callModule:handle_call/3
gen_server:multi_callModule:handle_call/3
gen_server:castModule:handle_cast/2
gen_server:abcastModule:handle_cast/2
-Module:handle_info/2
-Module:terminate/2
-Module:code_change/3
下面我们就按照其运行的生命周期,来看一下具体的实现。

进程启动

可以看到,gen_server在对外API中提供了start(start/3,start/4)和start_link(start_link/3, start_link/4)两组函数,start和start_link的最大区别在于,start启动的是一个单独的进程,不会加入到任何OTP监控树中,而start_link是可以被加入到监控树中的(当然得由Supervisor去启动才可以,两者建立link关系)。平时用的最多的会是start_link,而start基本场景是对某个模块进行单独的测试。

启动函数的实现:

start(Mod, Args, Options) ->
gen:start(?MODULE, nolink, Mod, Args, Options).

start(Name, Mod, Args, Options) ->
gen:start(?MODULE, nolink, Name, Mod, Args, Options).

start_link(Mod, Args, Options) ->
gen:start(?MODULE, link, Mod, Args, Options).

start_link(Name, Mod, Args, Options) ->
gen:start(?MODULE, link, Name, Mod, Args, Options).

可以看到四个函数都是将请求转发到了gen模块的start函数,而且start和start_link的差别在于第二个参数,start是nolink,start_link是link,再看看gen模块做了什么:

start(GenMod, LinkP, Name, Mod, Args, Options) ->
case where(Name) of
undefined ->
do_spawn(GenMod, LinkP, Name, Mod, Args, Options);
Pid ->
{error, {already_started, Pid}}
end.

start(GenMod, LinkP, Mod, Args, Options) ->
do_spawn(GenMod, LinkP, Mod, Args, Options).

do_spawn(GenMod, link, Mod, Args, Options) ->
Time = timeout(Options),
proc_lib:start_link(?MODULE, init_it,
[GenMod, self(), self(), Mod, Args, Options],
Time,
spawn_opts(Options));
do_spawn(GenMod, _, Mod, Args, Options) ->
Time = timeout(Options),
proc_lib:start(?MODULE, init_it,
[GenMod, self(), self, Mod, Args, Options],
Time,
spawn_opts(Options)).

do_spawn(GenMod, link, Name, Mod, Args, Options) ->
Time = timeout(Options),
proc_lib:start_link(?MODULE, init_it,
[GenMod, self(), self(), Name, Mod, Args, Options],
Time,
spawn_opts(Options));
do_spawn(GenMod, _, Name, Mod, Args, Options) ->
Time = timeout(Options),
proc_lib:start(?MODULE, init_it,
[GenMod, self(), self, Name, Mod, Args, Options],
Time,
spawn_opts(Options)).

...

timeout(Options) ->
case lists:keyfind(timeout, 1, Options) of
{_,Time} ->
Time;
false ->
infinity
end.

两个start的区别在于一个有Name,一个没有,如果指定了Name,会通过where函数检查进程是否已经注册了:

where({global, Name}) -> global:whereis_name(Name);
where({via, Module, Name}) -> Module:whereis_name(Name);
where({local, Name})  -> whereis(Name).

where会分三种情况去查询,global、via和local,global和local很好理解,{via, Module, Name}这个会是用在啥场景?思考了半天,觉着可能是这样,有时候可能global并不能满足我们的要求,比如可能是因为性能问题或者应用需要有自定义的namespace规则,这个时候我们就需要自己来实现服务发现逻辑了(比如通过zookeeper或者etcd),那么此时就可以通过这种via的方式,定义一个像global那样提供服务查询接口以的模块来进行查找,这块儿有OO语言里面Ducking Type的影子,哈哈。

然后link跟no_link的差别在于link是通过proc_lib:start_link创建的进程,而no_link是通过proc_lib:start创建的进程。这俩函数之前已经看过,都是通过同步的方式来创建新进程,只不过start_link建立的是link关系(这也是通过这种方式能加入OTP监控树的原因),而start建立的是monitor关系。

另外可以看到,我们在这里能够通过Option参数来控制新建进程的生存时间,lists:keyfind(Key, N, TupleList)是一个很有意思的函数,它检查一个由元组所构成的列表TupleList,如果其中一个元组的第N个元素为Key,那么就会返回,如果找不到会返回false,这个在解析配置时会很方便:

1> L = [{a, 1}, {b, 2}, {c, 3}].
[{a,1},{b,2},{c,3}]
2> lists:keyfind(b, 1, L).
{b,2}

接下来重点看新建进程逻辑init_it做了什么了,gen模块里面的init_it实现:

init_it(GenMod, Starter, Parent, Mod, Args, Options) ->
init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options).

init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
case register_name(Name) of
true ->
init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options);
{false, Pid} ->
proc_lib:init_ack(Starter, {error, {already_started, Pid}})
end.

init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
GenMod:init_it(Starter, Parent, Name, Mod, Args, Options).

...

register_name({local, Name} = LN) ->
try register(Name, self()) of
true -> true
catch
error:_ ->
{false, where(LN)}
end;
register_name({global, Name} = GN) ->
case global:register_name(Name, self()) of
yes -> true;
no -> {false, where(GN)}
end;
register_name({via, Module, Name} = GN) ->
case Module:register_name(Name, self()) of
yes ->
true;
no ->
{false, where(GN)}
end.

发现gen模块的init_it就做了一点微小的事情:没有指定服务进程名称,甩锅给GenMod(在这里也就是gen_server)的init_it;如果指定了服务进程名,则对服务进行注册并检查之前是否注册过,注册过直接终止,没注册过则甩锅给gen_server的init。在这里我们可以看见gen模块进程创建的一个逻辑,如果我们为进程指定了Name,那么在相应的范围(local、via、global)只能创建一个进程,如果不指定Name,那么进程创建数目会不受限制。

突然感觉这里跟Spring的Bean scope很像,比如spring中的Bean可以是application范围单例的(就相当于这里的global),也可以是用户会话级别的,也可以是请求级别的,或者是prototype,每次用到都创建最新的实例,还可以自定义bean scope,有意思!

既然gen模块都最终把锅甩给了gen_server的init_it,那我们再回头看看gen_server中init_it的实现:

init_it(Starter, self, Name, Mod, Args, Options) ->
init_it(Starter, self(), Name, Mod, Args, Options);
init_it(Starter, Parent, Name0, Mod, Args, Options) ->
Name = gen:name(Name0),
Debug = gen:debug_options(Name, Options),
HibernateAfterTimeout = gen:hibernate_after(Options),

case init_it(Mod, Args) of
{ok, {ok, State}} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, infinity, HibernateAfterTimeout, Debug);
{ok, {ok, State, Timeout}} ->
proc_lib:init_ack(Starter, {ok, self()}),
loop(Parent, Name, State, Mod, Timeout, HibernateAfterTimeout, Debug);
{ok, {stop, Reason}} ->
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, {error, Reason}),
exit(Reason);
{ok, ignore} ->
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, ignore),
exit(normal);
{ok, Else} ->
Error = {bad_return_value, Else},
proc_lib:init_ack(Starter, {error, Error}),
exit(Error);
{'EXIT', Class, Reason, Stacktrace} ->
gen:unregister_name(Name0),
proc_lib:init_ack(Starter, {error, terminate_reason(Class, Reason, Stacktrace)}),
erlang:raise(Class, Reason, Stacktrace)
end.
init_it(Mod, Args) ->
try
{ok, Mod:init(Args)}
catch
throw:R -> {ok, R};
Class:R -> {'EXIT', Class, R, erlang:get_stacktrace()}
end.

总体上看来,就是回调业务模块的init函数,等待其返回结果并进行判断,决定下一步的走向。如果init返回正常,就会向父进程发送ack消息(父进程会结束阻塞,也就是说,在init执行完毕前,父进程一直是阻塞的),然后以带超时和不带超时两种不同的方式进入到事件循环,如果是其它情况,则会取消注册,向父进程发送ack消息,然后按情况抛出不同的异常。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Erlang