Erlang的巨大优势一部分来自于其并发和分布式特性,还有一部分来自其错误处理能力,OTP框架则是第三部分
在一个服务器框架中,我们通常需要解决的问题有:进程(服务)命名、超时配置、调试信息、非期望消息处理、代码热加载、特殊错误的处理、公共回复代码、服务器关闭的处理、保证服务器和监督者的配合等。自己动手解决这些问题是一件有风险的事情,很幸运,Erlang/OTP已经在
gen_server
行为中解决了所有这些问题OTP的
gen_server
行为会要求提供一些进程的初始化和结束、基于消息发送的同步和异步处理以及其他任务的处理函数init/1
函数负责初始化服务器的状态,并完成服务器需要的所有一次性任务。这个函数可以返回{ok, State}
、{ok, State, TimeOut}
、{ok, State, hibernate}
、{stop, Reason}
以及ignore
常规的
{ok, State}
返回值无需解释,只要记住State
会直接传给进程的主循环,并作为进程的状态一直保存在那里就行了。当期望服务器在某个时间期限之前能收到一条消息时,可以使用
TimeOut
变量。如果到期没有收到任何消息,那么会给服务器发送一条特殊的消息(原子timeout
),可以在handle_info/2
中处理这条消息。很少会在产品代码使用这个选项,因为不能总是知道会收到哪条消息,而任意一条消息都会重置计时器。通常,更好的方法是使用erlang:start_timer/3
之类的函数,可以获得更好的处理控制如果确实觉得进程在很长一段时间内不会有什么消息要处理,并且担心内存问题,那么可以在元组中使用
hibernate
原子。一般来讲,hibernate
选项会缩减进程的状态,直到它收到一条消息,不过会多耗费些处理能力。如果在是否使用hibernate
选项时存有疑惑,就说明不太需要它如果在初始化的过程中出现了错误,可以返回
{stop, Reason}
注意!当执行
init/1
函数时,创建服务器的进程会被阻塞。这是因为它在等待一条“就绪”消息,这条消息由gen_server
模块自动发送以确认一切正常handle_call/2
函数用于处理同步消息。它有3个参数:Request
、From
以及State
在
gen_server
中,有8种不同的返回值可供选择,这些返回值都是元组形式{reply, Reply, NewState}
{reply, Reply, NewState, TimeOut}
{reply, Reply, NewState, hibernate}
{noreply, NewState}
{noreply, NewState, TimeOut}
{noreply, NewState, hibernate}
{stop, Reason, Reply, NewState}
{stop, Reason, NewState}
这些返回值中,
TimeOut
和hibernate
的工作方式和init/1
中的一样。Reply
中的内容会被原封不动地发回给调用服务器的进程共有3种不同的
noreply
选项,当使用noreply
时,服务器的通用部分会认为你将自己发送回应消息,可以调用gen_server:reply/2
发送回应在绝大部分情况下,只需要使用
reply
元组。不过有些情况确实需要使用noreply
,例如,希望由另一个进程来替你发送回应,或者想先发送一条确认消息(“嗨!我收到消息了!”),然后继续处理(处理完无需回应)。如果这是所需要的场景,那么只能使用gen_server:reply/2
,否则,调用会超时然后崩溃handle_cast
函数用于异步消息的处理,它的参数是:Message
和State
。和handle_call/3
类似,其中也可以进行任何处理。不过它只能返回noreply
元组{noreply, NewState}
{noreply, NewState, TimeOut}
{noreply, NewState, hibernate}
{stop, Reason, NewState}
handle_info
函数用于处理和接口不相容的消息。它和handle_case/2
非常类似,事实上,返回值也完全一样。它们之间的区别在于,这个回调函数只用来处理直接通过!
操作符发送的消息,以及如init/1
中timeout
、监控器通知或者EXIT
信号之类的特殊消息当上面3种
handle_something
函数返回形如{stop, Reason, NewState}
或者{stop, Reason, Reply, NewState}
的元组时,会调用terminate/2
函数。它有两个参数:Reason
和State
,分别对应stop
元组中的同名字段当父进程(创建服务器的进程)死亡时,也会调用
terminate/2
函数,不过这只会发生在gen_server
捕获了退出信号的时候如果在调用
terminate/2
时,原因不是normal
、shutdown
或者{shutdown, Term}
,那么OTP框架会把这当成故障,并会把进程的状态、故障原因、最后收到的消息等记入日志。这让调试变得更加容易,可以帮助你快速定位问题这个函数和
init/1
正好相反,因此所有在init/1
中做的动作都应该在terminate/2
中有对应的取消动作code_change
函数用于代码升级,它的调用形式是code_change(PreviousVersion, State, Extra)
。其中,变量PreviousVersion
在升级时是版本数据项本身,在降级时是{down, Version}
。State
变量中保存着服务器当前的所有状态数据,可以对其进行转换假如我们一开始使用一个有序字典来存储所有数据。一段时间之后,有序字典变得越来越慢,我们决定用常规字典把它替换掉。为了避免进程在接下来的调用中崩溃,可以在这个函数中安全地进行数据结构的转换。所要做的就是用
{ok, NewState}
返回新的状态gen_server
的调用与回调关系gen_server:start/3-4
<->YourModule:init/1
gen_server:start_link/3-4
<->YourModule:init/1
gen_server:call/2-3
<->YourModule:handle_call/3
gen_server:cast/2
<->YourModule:handle_cast/2
还有其他几个回调函数如
handle_info/2
、terminate/2
和code_change/3
,这些回调函数主要处理一些特殊情况gen_fsm
行为和gen_server
有点类似,因为gen_fsm
是gen_server
行为的一个专用版本。它们之间最大的区别在于,gen_fsm
中不再处理call
消息和cast
消息,而是处理同步和异步事件FSM中的
init
函数和通用服务器中使用的init/1
完全一样,除了返回值多一些,可接受的返回值为:{ok, StateName, Data}
、{ok, StateName, Data, Timeout}
、{ok, StateName, Data, hibernate}
以及{stop, Reason}
。stop
元组的工作原理和gen_server
中的完全一样,hibernate
和Timeout
的语义也保持不变StateName
是一个新出现的变量。StateName
是原子类型,表示下一个被调用的回调函数函数
StateName/2
和StateName/3
是占位名字,由你来决定它们的内容假设
init/1
函数返回元组{ok, sitting, dog}
,这意味着FSM会处于sitting状态在上面的FSM中,
init/1
函数的返回值表明我们该处于sitting
状态。当gen_fsm
进程收到一个事件时,函数sitting/2
或者sitting/3
会被调用。对于异步事件,会调用sitting/2
函数,对于同步事件,会调用sitting/3
函数函数
sitting/2
(或者一般的说,StateName/2
)有两个参数:一个是Event
,作为事件发送来的实际消息;一个是StateData
,调用携带的数据sitting/2
函数可以返回以下几种元组{next_state, NextStateName, NewStateData}
{next_state, NextStateName, NewStateData, Timeout}
{next_state, NextStateName, hibernate}
{stop, Reason, NewStateData}
函数
sitting/3
的参数与此类似,只是在Event
和StateData
之间多了一个From
参数。From
参数和gen_fsm:reply/2
的用法与gen_server
中的完全一样函数
StateName/3
可以返回如下元组{reply, Reply, NextStateName, NewStateData}
{reply, Reply, NextStateName, NewStateData, Timeout}
{reply, Reply, NextStateName, NewStateData, hibernate}
{next_state, NextStateName, NewStateData}
{next_state, NextStateName, NewStateData, Timeout}
{next_state, NextStateName, NewStateData, hibernate}
{stop, Reason, Reply, NewStateData}
{stop, Reason, NewStateData}
注意,这些函数数量不受限制,只要被导出就行。元组中的原子
NextStateName
决定了下一次会调用哪个函数无论当前在哪个状态中,全局事件都会触发一个特定反应。由于这类事件在每个状态中都会以同样的方式处理,因此
handle_event/3
回调函数正好满足需要。这个函数的参数和StateName/2
类似,不过它在中间多了一个参数StateName(handle_event(Event, StateName, Data))
,这个参数表明了收到事件时所处的状态。它的返回值和函数StateName/2
一样回调函数
handle_sync_event/4
和StateName/3
的关系与handle_event/2
和StateName/2
的关系一样。这个函数处理同步全局事件,参数和所返回的元组种类都和StateName/3
一样通过向FSM发送事件所使用的函数,我们可以知道一个事件是全局的还是针对某个特定状态的。被
StateName/2
函数处理的异步事件是用函数gen_fsm:send_event/2
发送的,而被StateName/3
函数处理的同步事件是用函数gen_fsm:sync_send_event/2-3
发送的(第三个可选的参数是超时)两个对等的用来发送全局事件的函数为:
gen_fsm:send_all_state_event/2
和gen_fsm:sync_send_all_state_event/2-3
FSM中
code_change
函数的工作方式和gen_server
中的完全一样,只是多处一个额外的状态参数,如:code_change(OldVersion, StateName, Data, Extra)
,并且返回的元组格式为{ ok, NextStateName, NewStateData }
同样的,FSM中
terminate
的行为和通用服务器中也类似,terminate(Reason, StateName, Data)
函数做多额工作应该和init/1
相反gen_event
行为与gen_server
以及gen_fsm
有很大的不同,它根本不需要实际启动一个进程。之所以不需要进程是因为它的工作方式是“接受一组回调函数”简单来讲,
gen_event
行为运行这个接受并调用回调函数的事件管理器进程,而你只需要提供包含这些回调函数的模块即可。这意味着你无需关心事件分派,只需按照事件管理器要求的格式放置回调函数即可。所有的事件管理就自然都有了,你只需提供应用特定的东西init
和terminate
函数与我们前面看到的gen_server
和gen_fsm
行为中的类似。init/1
函数接收列表参数,返回{ok, State}
。在init/1
中创建的东西,要在terminate/2
函数中有对应释放操作handle_event(Event, State)
函数可以说是gen_event
回调模块的核心函数。和gen_server
中的handle_cast/2
一样,handle_event/2
函数也是异步的。不过,它的返回值有所不同:{ok, NewState}
{ok, NewState, hibernate}
,让事件管理器进程进入休眠状态,直到收到下一个事件remove_handler
{swap_handler, Args1, NewState, NewHandler, Args2}
返回值
{ok, NewState}
元组的含义和gen_server:handle_cast/2
函数中的一样。它只更新自己的状态,不做任何回应。返回
{ok, NewState, hibernate}
则会使整个事件管理器进入休眠状态。记住,事件处理器和其他管理器运行在同一个进程中返回
remove_handler
则会导致事件处理器从事件管理器中删除。当某个事件处理器知道自己已经完成工作并且无其他任务时,可以使用这个返回值最后一个返回值
{swap_handler, Args1, NewState, NewHandler, Args2}
,它移除当前事件处理器并用一个新的替代它。事件管理器首先调用CurrentHandler:terminate(Args1, NewState)
函数,并移除当前的事件处理器。接着调用NewHandler:init(Args2, ResultFromTerminate)
函数,添加新的事件处理器。所有事件都是通过
gen_event:notify/2
函数触发的,和gen_server:cast/2
一样,它也是异步的。还有另外一个函数gen_event:sync_notify/2
,它是同步的。由于handle_event/2
是异步的,这里的同步指的是,当所有事件处理器都收到这个事件并且处理完毕后,sync_notify
函数才会返回。在那之前,事件管理器会一直阻塞调用进程,不予响应handle_call
函数和gen_server
的handle_call
回调函数类似,不同之处在于,它可以返回{ok, Reply, NewState}
、{ok, Reply, NewState, hibernate}
、{remove_handler, Reply}
以及{swap_handler, Reply, Args1, NewState, Handler2, Args2}
。使用gen_event:call/3-4
函数就可以发起该调用handle_info
回调和handle_event
回调非常相似(有着同样的返回值和含义),唯一的不同在于,handle_info
只处理带外消息,如退出信号或使用!
操作符直接向事件管理器进程发送消息。它的使用场景和gen_server
以及gen_fsm
中的handle_info
的使用场景类似code_change
函数的工作方式和gen_server
中的一样,不过它仅仅针对单独的事件处理器。它的参数为:OldVsn
、State
、Extra
,分别表示版本号、当前事件处理器的状态、最后这个参数——Extra
,目前可以不用关心。这个方法只要返回{ok, NewState}
即可在所有OTP中,监督者是最容易使用和理解的一个,但也是最难设计好的一个。在Erlang中应该把所有东西都监督起来,此外,监督机制还可以让你以恰当的顺序终止应用
当想终止一个应用时,只需去终止虚拟机中最顶层的那个监督者(调用
init:stop/1
函数就可以了)。然后这个监督者会接着要求它的子进程停止运行。如果某个子进程也是一个监督者,它会做同样的事情如果没有用树形结构来组织所有的进程,那么很难让VM以井然有序的方式终止。当然,有时也会出现进程因某种原因被卡住而不能正常终止的情况。此时,监督者可以强行杀死这个进程。
监督者使用起来很简单,我们只需要提供一个回调函数:
init/1
。麻烦的地方在于这个函数的返回值非常复杂。下面是一个返回值的例子:1
2
3
4
5
6
7
8
9
10
11
12
13{ok, {{one_for_all, 5, 60},
[{fake_id,
{fake_mod, start_link, [SomeArg]},
permanent,
5000,
worker,
[fake_mode]},
{other_id,
{event_manager_mod, start_link, []},
transient,
infinity,
worker,
dynamic}]}}重启策略
RestartStrategy
的值可以为:one_for_one
,当被监督的进程都是独立的、互不相关的,或者即便这些进程重启后丢失了自己的状态,也不会对其他进程产生影响时,可以使用one_for_one
策略one_for_all
,当所有工作者进程都受同一个监督者监督,且这些工作者进程必须互相依赖才能正常工作时,就使用这个策略rest_for_one
,如果一个进程死了,那么所有在这个进程之后启动的进程(依赖于该进程)都将被重启,反之不然simple_one_for_one
,这个类型的监督者只监督一种子进程,当希望以动态的方式向监督者中增加子进程(当需要一个新的子进程时,向它发起请求,就能得到一个),而不是静态启动子进程时,可以使用这种策略
RestartStrategy
元组中剩余的两个变量是MaxRestart
和MaxTime
。他们的意思是,如果在MaxTime
(以秒为单位)指定的时间内,重启次数超过了MaxRestart
指定的数字,那么监督者会放弃重启并终止所有子进程,然后自杀,永远停止运行子进程规格说明可以描述成:
{ChildId, StartFunc, Restart, Shutdown, Type, Modules}
ChildId
只是监督者内部使用的一个名称StartFunc
是一个元组,用来指定子进程的启动方式,它采用了标准的{M, F, A}
格式。注意!这里的启动函数是OTP兼容的,在执行时会和调用者进程链接在一起,这一点非常重要Restart
指定了监督者在某个特定的子进程死后的处理方式,它可以取如下3个值:permanent
: 不管发生什么,一个永久进程都要被重启temporary
: 指的是那种绝对不应该被重启的进程transient
: 介于上述两种进程之间。如果被正常终止了,就不会被重启。如果异常死亡,就会被重启Shutdown
当要求最顶层的监督者终止时,它会对每个子进程调用exit(ChildPid, shutdown)
。如果这个子进程是一个工作者进程并且捕获了退出信号,那么就会调用自己的terminate
函数;否则,进程死掉就行了。如果是一个监督者子进程收到了shutdown
信号,它会用同样的方式将这个信号转发给它的子进程子进程规格说明中的
Shutdown
值用来指定终止的超时期限,它可以设置一个确定的终止超时,可以是多少毫秒,也可以是infinity
。如果指定时间过去了,进程还没有死,那么进程会被exit(Pid, kill)
强行杀死如果对子进程并不在意,不设定超时等待时间时,子进程死亡也没啥影响,那么可以将
Shutdown
设置成原子brutal_kill
。使用brutal_kill
会调用exit(Pid, kill)
杀死子进程,此时,退出是即时的,子进程也无法捕获这个退出信号Type
字段可以让监督者知道子进程是一个监督者(supervisor)(实现了supervisor
或者supervisor_bridge
行为)还是一个工作者(worker)(任何其他OTP进程)Modules
是一个列表,其中只有一个元素:子进程行为使用的回调模块名。有一个例外情况:事先无法知道回调模块的标示符(如事件管理器中的事件处理器模块)。此时,Modules
的值要设置成dynamic
,这样,在使用其他高级特性(如发布)时,整个OTP系统才能知道去找谁动态监督使用
one_for_one
、rest_for_one
或者one_for_all
策略把工作者进程加入监督者中时,除了该进程的pid
和其他一些信息外,还会向监督者持有的一个列表中增加子进程规格说明。在以后重启子进程或者执行其他任务时,会使用这份子进程规格说明。基于这种工作方式,相应的接口定义如下:start_child(SupervisorNameOrPid, ChildSpec)
向列表中增加一个子进程规格说明,并且用该规格说明启动一个子进程terminate_child(SupervisorNameOrPid, ChildId)
终止或者强行杀死(brutal_kills
)指定的子进程。子进程的规格说明仍然保留在监督者中restart_child(SupervisorNameOrPid, ChildId)
使用子进程规格说明重启子进程delete_child(SupervisorNameOrPid, ChildId
删除指定ChildId
所对应的子进程规格说明check_childspecs([ChildSpec])
检查一个子进程规格说明是否有效。在调用start_child/2
函数前,可以用这个函数去测试一下规格说明的有效性count_children(SupervisorNameOrPid)
分类列举出该监督者下的所有子进程,包括活动进程个数、子进程规格说明个数、监督者类型的个数和工作者类型的个数which_children(SupervisorNameorPid)
返回一个指定监督者下所有子进程信息的列表
当子进程不多时,这些函数很适用于各种动态性要求(启动、终止等)。不过,由于内部使用的是列表,因此当需要快速访问大量子进程时,并不是很适用。此时所需要的是
simple_one_for_one
使用
simple_one_for_one
策略的监督者把所有子进程信息存放在一个字典中,这样可以快速查找,并且对监督者的所有子进程来说,只有一份子进程规格说明编写
simple_one_for_one
策略监督者的方式基本上和其他策略的监督者类似,只有一点不同:{M, F, A}
元组中的参数列表A
并不是全部参数,完整的参数是把supervisor:start_child(Sup, Args)
调用中的Args
追加到A
之后的新列表这里的
supervisor:start_child/2
的含义改变了。与原来的supervisor:start_child(Sup, Spec)
调用erlang:apply(M, F, A)
不同,现在的supervisor:start_child(Sup, Args)
调用的是erlang:apply(M, F, A++Args)
1
2
3
4
5
6init(jamband) ->
{ok, {{simple_one_for_one, 3, 60},
[{jam_musician,
{musicians, start_link, []},
temporary, 1000, worker, [musicians]}
]}}仅当明确知道要监督的子进程数量不多并且(或者)不需要频繁地操控子进程,或者对性能要求不高的情况下,可以动态地使用标准监督者。对于其他需要动态监督的情况,尽可能使用
simple_one_for_one
Erlang极简学习笔记<10>——OTP篇