谁监督监督者?
从糟糕到优秀
监督者是 OTP 中最实用的部分之一,你将会经常用到它们。我们在错误和进程和设计并发应用程序中已经见过基本的监督者。我们已经了解到,它们可以通过重启有问题的进程来保证软件的持续运行。
更详细地说,我们的监督者会启动一个工作者进程,与其链接,并使用process_flag(trap_exit,true)
捕获退出信号,以便在进程死亡时重启它。这在我们希望重启进程时没问题,但它也很笨。想象一下,你用遥控器打开电视,如果第一次没成功,你可能会尝试一两次,以防你没有按对按钮或者信号出了问题。我们的监督者,如果它试图打开这台电视,会一直尝试,即使它发现遥控器没电了或者根本不匹配这台电视。这真是一个很笨的监督者。
我们监督者的另一个缺点是,它们一次只能监视一个工作者。别误会,有时为单个工作者设置一个监督者是有用的,但在大型应用程序中,这意味着你只能拥有一个监督者链,而不是树形结构。如何监督需要 2 个或 3 个工作者同时执行的任务?用我们目前的实现,这是做不到的。
幸运的是,OTP 监督者提供了处理此类情况(以及更多情况)的灵活性。它们允许你定义在放弃之前,在给定时间段内应该重启工作者多少次。它们允许你为每个监督者设置多个工作者,甚至允许你在几种模式之间进行选择,以确定在发生故障时,它们之间应该如何依赖彼此。
监督者概念
监督者是最容易使用和理解的行为之一,但也是最难设计好的行为之一。关于监督者和应用程序设计,存在多种策略,但在深入探讨之前,我们需要理解一些更基本的概念,因为否则会很困难。
我在文本中反复使用的“工作者”一词,没有太多定义。工作者的定义与监督者正好相反。如果监督者应该是只负责确保子进程在死亡时重启的进程,那么工作者就是负责实际工作的进程,它们在执行任务时可能会死亡。通常情况下,我们不信任工作者。
监督者可以监督工作者和其他监督者,而工作者只能在另一个监督者的管理下使用。
为什么每个进程都应该受到监督?原因很简单:如果你出于某种原因产生了不受监督的进程,你如何确保它们已经消失或没有消失?如果你不能测量某样东西,它就不存在。现在,如果一个进程存在于你的所有监督树之外的虚空中,你怎么知道它存在或不存在?它是怎么到那里的?还会发生吗?
如果发生这种情况,你会发现你的内存泄漏非常缓慢。慢到你的虚拟机可能会突然死亡,因为它不再有内存,慢到你可能难以追踪它,直到它一次又一次地发生。当然,你可能会说“如果我小心点,知道自己在做什么,一切都将顺利进行”。也许会顺利进行,也许不会。在生产系统中,你不想冒险,在 Erlang 的情况下,这就是你从一开始就使用垃圾回收的原因。保持进程受到监督非常有用。
另一个好处是,它可以让你以正确的顺序终止应用程序。你会编写一些并非要永远运行的 Erlang 软件。但是,你仍然希望它能干净地终止。你如何知道所有内容都已准备就绪,可以关闭?有了监督者,这很容易。每当你想要终止一个应用程序时,你可以让 VM 的顶级监督者关闭(这会使用像init:stop/1
这样的函数为你完成)。然后,该监督者会要求它的每个子进程终止。如果有些子进程是监督者,它们也会做同样的事情。
这会让你获得一个井然有序的 VM 关闭,如果没有将所有进程包含在树中,这是很难做到的。
当然,有时你的进程会因为某些原因卡住,无法正确终止。在这种情况下,监督者可以通过暴力方式杀死进程。
这就是关于监督者基本理论的全部内容。我们有工作者、监督者、监督树、指定依赖关系的不同方法、告诉监督者何时放弃尝试或等待子进程的方法等等。这并非监督者的全部功能,但目前,这将让我们涵盖实际使用它们所需的基本内容。
使用监督者
到目前为止,这一章充满了暴力:父母们把时间花在把孩子绑在树上,强迫他们工作,然后无情地杀死他们。如果没有实际实现这一切,我们就不是真正的虐待狂。
当我提到监督者很容易使用时,我并没有开玩笑。只有一个回调函数需要提供:init/1
。它接受一些参数,仅此而已。问题是它会返回一个非常复杂的东西。这是一个监督者返回的示例。
{ok, {{one_for_all, 5, 60}, [{fake_id, {fake_mod, start_link, [SomeArg]}, permanent, 5000, worker, [fake_mod]}, {other_id, {event_manager_mod, start_link, []}, transient, infinity, worker, dynamic}]}}.
什么?是的,这确实很复杂。一个通用的定义可能会更易于理解
{ok, {{RestartStrategy, MaxRestart, MaxTime},[ChildSpecs]}}.
其中 ChildSpec 代表子进程规范。 RestartStrategy 可以是 one_for_one
、rest_for_one
、one_for_all
和 simple_one_for_one
中的任何一个。
one_for_one
One for one 是一种直观的重启策略。它基本上意味着,如果你的监督者监督多个工作者,其中一个工作者失败了,只有那个工作者应该被重启。只要被监督的进程是独立的,彼此之间没有真正联系,或者进程可以重启并丢失其状态而不会影响其兄弟进程,你应该使用 one_for_one
。
one_for_all
One for all 与三剑客没什么关系。它应该在所有进程在一个监督者管理下,彼此之间高度依赖才能正常工作的情况下使用。假设你决定在与有限状态机抗争一章中实现的交易系统之上添加一个监督者。如果其中一个交易者崩溃,重启其中一个交易者实际上没有意义,因为它们的状态会不同步。同时重启它们两者会更加明智,one_for_all
就是适合这种情况的策略。
rest_for_one
这是一种更具体的策略。每当你必须启动在链中相互依赖的进程时(A 启动 B,B 启动 C,C 启动 D 等等),你可以使用 rest_for_one
。它在服务中也很有用,在服务中你拥有类似的依赖关系(X 独自工作,但 Y 依赖于 X,Z 依赖于两者)。rest_for_one
重启策略基本上会使它,如果一个进程死亡,所有在它之后启动的进程(依赖于它的进程)都会重启,但反过来则不会。
simple_one_for_one
simple_one_for_one
重启策略并不是最简单的。我们将在使用它时更详细地了解它,但它基本上会使它只能接受一种类型的子进程,并且在你希望动态地将它们添加到监督者中时使用它,而不是在启动时静态地启动它们。
换句话说,simple_one_for_one
监督者只是坐在那里,它知道它只能产生一种类型的子进程。每当你想要一个新的子进程时,你都会请求它,然后你就会得到它。理论上,可以使用标准的 one_for_one
监督者来完成这种操作,但使用简单版本有一些实际优势。
注意:one_for_one
和 simple_one_for_one
之间的最大区别之一是,one_for_one
会保存一个它拥有的所有子进程(以及它曾经拥有过的子进程,如果你没有清除它)的列表,这些子进程按顺序启动,而 simple_one_for_one
保存一个所有子进程的单一定义,并使用 dict
来保存其数据。基本上,当一个进程崩溃时,当你拥有大量子进程时,simple_one_for_one
监督者的速度会快得多。
重启限制
RestartStrategy 元组的最后一部分是变量 MaxRestart 和 MaxTime。这个想法基本上是,如果在 MaxTime(以秒为单位)内发生了超过 MaxRestart 次重启,监督者就会放弃你的代码,将其关闭,然后自杀,永远不会返回(这就是它有多糟糕)。幸运的是,该监督者的监督者可能仍然对它的子进程抱有希望,并且会重新启动它们。
子进程规范
现在我们来了解返回值的 ChildSpec 部分。 ChildSpec 代表子进程规范。之前我们有以下两个 ChildSpec
[{fake_id, {fake_mod, start_link, [SomeArg]}, permanent, 5000, worker, [fake_mod]}, {other_id, {event_manager_mod, start_link, []}, transient, infinity, worker, dynamic}]
子进程规范可以用更抽象的形式描述为
{ChildId, StartFunc, Restart, Shutdown, Type, Modules}.
ChildId
ChildId 只是监督者内部使用的一个内部名称。你很少需要自己使用它,尽管它可能对调试和当你决定实际获取监督者所有子进程的列表时有用。任何项都可以用作 Id。
StartFunc
StartFunc 是一个元组,它告诉如何启动子进程。它是我们之前已经使用过几次的标准 {M,F,A}
格式。请注意,这里面的启动函数必须符合 OTP 标准,并且在执行时与调用者链接(提示:始终使用 gen_*:start_link()
包裹在你自己模块中)。
Restart
Restart 告诉监督者当特定的子进程死亡时如何反应。它可以取三个值
- permanent
- temporary
- transient
一个永久进程应该始终重启,无论发生什么。我们在之前的应用程序中实现的监督者只使用这种策略。它通常由运行在你的节点上的重要、长寿命进程(或服务)使用。
另一方面,一个临时进程是永远不应该重启的进程。它们用于短寿命的工作者,这些工作者预计会失败,并且只有很少的代码依赖于它们。
瞬态进程介于两者之间。它们应该一直运行,直到正常终止,然后它们不会重启。但是,如果它们因异常原因死亡(退出原因不是 normal
),它们将被重启。这种重启选项通常用于需要完成任务的工作者,但在完成后不会再使用。
你可以在单个监督者管理下混合使用这三种类型的子进程。这可能会影响重启策略:one_for_all
重启不会因临时进程死亡而触发,但如果永久进程首先死亡,那么这个临时进程可能会在同一个监督者下重启!
Shutdown
在之前的文本中,我提到过可以使用监督者关闭整个应用程序。这就是它实现的方式。当顶级监督者被要求终止时,它会对每个 Pid 调用 exit(ChildPid, shutdown)
。如果子进程是工作者并且正在捕获退出信号,它会调用自己的 terminate
函数。否则,它只会死亡。当监督者接收到 shutdown
信号时,它会以相同的方式将其转发给自己的子进程。
子规格的Shutdown值用于设置终止的截止时间。在某些工作进程中,您可能需要做一些事情,例如正确关闭文件,通知服务您正在离开等等。在这些情况下,您可能希望使用特定的截止时间,以毫秒为单位或infinity
(如果您非常耐心)。如果时间过去而没有任何事情发生,则进程将被暴力杀死,使用exit(Pid, kill)
。如果您不关心子进程,并且它可以在没有任何超时的情况下毫无后果地死亡,那么原子brutal_kill
也是一个可接受的值。brutal_kill
将使子进程被exit(Pid, kill)
杀死,这是不可捕获的且瞬时的。
选择合适的Shutdown值有时很复杂或棘手。如果您有一系列具有Shutdown值的监管者,例如:5000 -> 2000 -> 5000 -> 5000
,最后两个可能最终会被暴力杀死,因为第二个有更短的截止时间。这完全取决于应用程序,关于这个主题很少有普遍的建议。
注意:请注意,simple_one_for_one
子进程不遵循此规则,即Shutdown时间。在simple_one_for_one
的情况下,监管者只会退出,然后由每个工作进程在监管者消失后自行终止。
类型
类型只是让监管者知道子进程是工作进程还是监管者。这在使用更高级的 OTP 功能升级应用程序时非常重要,但您现在不需要关心这一点 - 只需说出真相,一切都会好起来的。您必须信任您的监管者!
模块
Modules是一个包含一个元素的列表,即子进程行为使用的回调模块的名称。例外情况是,当您有回调模块,而您事先不知道其标识时(例如,事件管理器中的事件处理程序)。在这种情况下,Modules的值应该是dynamic
,以便整个 OTP 系统知道在使用更高级的功能时联系谁,例如发布。
更新
从版本 18.0 开始,监管者结构可以作为映射提供,形式为{#{strategy => RestartStrategy, intensity => MaxRestart, period => MaxTime}, [#{id => ChildId, start => StartFunc, restart => Restart, shutdown => Shutdown, type => Type, modules => Module}}
。
这与现有结构几乎相同,但使用映射而不是元组。supervisor
模块定义了一些默认值,但为了清晰和可读性,以便维护您的代码的人员能够理解,明确地提供整个规范是一个好主意。
万岁,我们现在拥有开始监管进程所需的必要基础知识。您可以休息一下,消化所有内容,或者继续学习更多内容!
测试它
需要一些实践。在实践方面,完美的例子是乐队排练。好吧,并不完美,但请耐心等待,因为我们将以一个类比作为先例,尝试编写监管者等等。
我们正在管理一支名为*RSYNC的乐队,由程序员演奏一些常见的乐器:鼓手、歌手、贝斯手和键盘手,以纪念所有被遗忘的 80 年代辉煌。尽管有一些复古热门歌曲翻唱,例如“线程安全之舞”和“周六夜间编码者”,但乐队很难找到演出场所。对整个情况感到恼火,我带着另一个糖瘾带来的想法冲进你的办公室,在 Erlang 中模拟一支乐队,因为“至少我们不会听到我们的家伙”。你很累,因为你和鼓手住在同一个公寓(他是乐队中最薄弱的环节,但他们和他在一起,因为他们不认识其他鼓手,说实话),所以你同意了。
音乐家
我们可以做的第一件事是编写单个乐队成员。对于我们的用例,音乐家模块将实现一个gen_server
。每个音乐家将接受一种乐器和一个技能等级作为参数(这样我们就可以说鼓手很烂,而其他人还不错)。一旦音乐家生成,他们就开始演奏。我们还将有一个选项,如果需要,可以停止他们。这给了我们以下模块和接口
-module(musicians). -behaviour(gen_server). -export([start_link/2, stop/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]). -record(state, {name="", role, skill=good}). -define(DELAY, 750). start_link(Role, Skill) -> gen_server:start_link({local, Role}, ?MODULE, [Role, Skill], []). stop(Role) -> gen_server:call(Role, stop).
我已经定义了一个?DELAY
宏,我们将把它用作每个音乐家展示自己正在演奏的时间之间的标准时间跨度。正如记录定义所示,我们还必须为他们每个人提供一个姓名
init([Role, Skill]) -> %% To know when the parent shuts down process_flag(trap_exit, true), %% sets a seed for random number generation for the life of the process %% uses the current time to do it. Unique value guaranteed by now() random:seed(now()), TimeToPlay = random:uniform(3000), Name = pick_name(), StrRole = atom_to_list(Role), io:format("Musician ~s, playing the ~s entered the room~n", [Name, StrRole]), {ok, #state{name=Name, role=StrRole, skill=Skill}, TimeToPlay}.
init/1
函数中有两件事要做。首先,我们开始捕获退出。如果您还记得通用服务器章节中terminate/2
的描述,如果我们希望在服务器的父进程关闭其子进程时调用terminate/2
,则需要这样做。init/1
函数的其余部分是设置一个随机种子(以便每个进程获得不同的随机数),然后为自己创建一个随机名称。用于创建名称的函数是
%% Yes, the names are based off the magic school bus characters' %% 10 names! pick_name() -> %% the seed must be set for the random functions. Use within the %% process that started with init/1 lists:nth(random:uniform(10), firstnames()) ++ " " ++ lists:nth(random:uniform(10), lastnames()). firstnames() -> ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha", "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"]. lastnames() -> ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin", "Terese", "Tennelli", "Jamal", "Li", "Perlstein"].
好了!我们可以继续进行实现。对于handle_call
和handle_cast
来说,这将非常简单
handle_call(stop, _From, S=#state{}) -> {stop, normal, ok, S}; handle_call(_Message, _From, S) -> {noreply, S, ?DELAY}. handle_cast(_Message, S) -> {noreply, S, ?DELAY}.
我们唯一可用的调用是停止音乐家服务器,我们同意很快做到这一点。如果我们收到一条意外消息,我们不会回复它,并且调用者将崩溃。这不是我们的问题。我们在{noreply, S, ?DELAY}
元组中设置超时,原因很简单,我们现在将看到
handle_info(timeout, S = #state{name=N, skill=good}) -> io:format("~s produced sound!~n",[N]), {noreply, S, ?DELAY}; handle_info(timeout, S = #state{name=N, skill=bad}) -> case random:uniform(5) of 1 -> io:format("~s played a false note. Uh oh~n",[N]), {stop, bad_note, S}; _ -> io:format("~s produced sound!~n",[N]), {noreply, S, ?DELAY} end; handle_info(_Message, S) -> {noreply, S, ?DELAY}.
每次服务器超时时,我们的音乐家都会演奏一个音符。如果他们很优秀,一切都会完全正常。如果他们很糟糕,他们有五分之一的机会失误并演奏一个错误的音符,这将导致他们崩溃。同样,我们在每次非终止调用结束时设置?DELAY
超时。
然后,我们添加一个空的code_change/3
回调,这是“gen_server”行为所必需的
code_change(_OldVsn, State, _Extra) -> {ok, State}.
我们可以设置终止函数
terminate(normal, S) -> io:format("~s left the room (~s)~n",[S#state.name, S#state.role]); terminate(bad_note, S) -> io:format("~s sucks! kicked that member out of the band! (~s)~n", [S#state.name, S#state.role]); terminate(shutdown, S) -> io:format("The manager is mad and fired the whole band! " "~s just got back to playing in the subway~n", [S#state.name]); terminate(_Reason, S) -> io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]).
这里我们有很多不同的消息。如果我们以normal
原因终止,这意味着我们已经调用了stop/1
函数,因此我们显示了音乐家自愿离开。在bad_note
消息的情况下,音乐家将崩溃,我们将说这是因为经理(我们将很快添加的监管者)将他踢出了游戏。
然后是shutdown
消息,它将来自监管者。每当这种情况发生时,这意味着监管者决定杀死所有子进程,或者在我们的案例中,解雇所有音乐家。然后,我们为其余部分添加一个通用的错误消息。
这里是一个简单的音乐家用例
1> c(musicians). {ok,musicians} 2> musicians:start_link(bass, bad). Musician Ralphie Franklin, playing the bass entered the room {ok,<0.615.0>} Ralphie Franklin produced sound! Ralphie Franklin produced sound! Ralphie Franklin played a false note. Uh oh Ralphie Franklin sucks! kicked that member out of the band! (bass) 3> =ERROR REPORT==== 6-Mar-2011::03:22:14 === ** Generic server bass terminating ** Last message in was timeout ** When Server state == {state,"Ralphie Franklin","bass",bad} ** Reason for termination == ** bad_note ** exception error: bad_note
所以 Ralphie 在弹奏并演奏了一个错误的音符后崩溃了。万岁。如果您尝试使用good
音乐家做同样的事情,您需要调用我们的musicians:stop(Instrument)
函数才能停止所有演奏。
乐队监管者
我们现在可以处理监管者了。我们将有三种等级的监管者:一个宽容的监管者,一个愤怒的监管者,以及一个彻头彻尾的混蛋。他们之间的区别在于,宽容的监管者虽然仍然是一个非常爱发牢骚的人,但他会一次解雇乐队中的一名成员(one_for_one
),即失败的那个人,直到他厌烦了,解雇了他们所有人,放弃了乐队。另一方面,愤怒的监管者会在每次犯错时解雇一些人(rest_for_one
),并且会在解雇所有成员并放弃之前等待更短的时间。然后,混蛋监管者会在有人犯错时解雇整个乐队,如果乐队失败的次数更少,他们就会放弃。
-module(band_supervisor). -behaviour(supervisor). -export([start_link/1]). -export([init/1]). start_link(Type) -> supervisor:start_link({local,?MODULE}, ?MODULE, Type). %% The band supervisor will allow its band members to make a few %% mistakes before shutting down all operations, based on what %% mood he's in. A lenient supervisor will tolerate more mistakes %% than an angry supervisor, who'll tolerate more than a %% complete jerk supervisor init(lenient) -> init({one_for_one, 3, 60}); init(angry) -> init({rest_for_one, 2, 60}); init(jerk) -> init({one_for_all, 1, 60});
init 定义并没有到此结束,但这让我们可以为我们想要的每种类型的监管者设定基调。宽容的监管者只会重启一名音乐家,并在 60 秒内发生四次失败时才会失败。第二个监管者只接受 2 次失败,而混蛋监管者对这方面的标准非常严格!
现在让我们完成这个函数,并实际实现乐队启动函数等等
init({RestartStrategy, MaxRestart, MaxTime}) -> {ok, {{RestartStrategy, MaxRestart, MaxTime}, [{singer, {musicians, start_link, [singer, good]}, permanent, 1000, worker, [musicians]}, {bass, {musicians, start_link, [bass, good]}, temporary, 1000, worker, [musicians]}, {drum, {musicians, start_link, [drum, bad]}, transient, 1000, worker, [musicians]}, {keytar, {musicians, start_link, [keytar, good]}, transient, 1000, worker, [musicians]} ]}}.
所以我们可以看到我们将有 3 名优秀的音乐家:歌手、贝斯手和键盘手。鼓手很糟糕(这让你很生气)。音乐家有不同的Restart(永久、瞬时或临时),因此即使当前的歌手自愿离开,乐队也无法没有歌手工作,但仍然可以没有贝斯手演奏得很好,因为坦率地说,谁在乎贝斯手呢?
这给了我们一个功能性的乐队监管者模块,我们现在可以尝试一下
3> c(band_supervisor). {ok,band_supervisor} 4> band_supervisor:start_link(lenient). Musician Carlos Terese, playing the singer entered the room Musician Janet Terese, playing the bass entered the room Musician Keesha Ramon, playing the drum entered the room Musician Janet Ramon, playing the keytar entered the room {ok,<0.623.0>} Carlos Terese produced sound! Janet Terese produced sound! Keesha Ramon produced sound! Janet Ramon produced sound! Carlos Terese produced sound! Keesha Ramon played a false note. Uh oh Keesha Ramon sucks! kicked that member out of the band! (drum) ... <snip> ... Musician Arnold Tennelli, playing the drum entered the room Arnold Tennelli produced sound! Carlos Terese produced sound! Janet Terese produced sound! Janet Ramon produced sound! Arnold Tennelli played a false note. Uh oh Arnold Tennelli sucks! kicked that member out of the band! (drum) ... <snip> ... Musician Carlos Frizzle, playing the drum entered the room ... <snip for a few more firings> ... Janet Jamal played a false note. Uh oh Janet Jamal sucks! kicked that member out of the band! (drum) The manager is mad and fired the whole band! Janet Ramon just got back to playing in the subway The manager is mad and fired the whole band! Janet Terese just got back to playing in the subway The manager is mad and fired the whole band! Carlos Terese just got back to playing in the subway ** exception error: shutdown
魔法!我们可以看到只有鼓手被解雇了,过了一会儿,每个人都明白过来了。然后他们就坐地铁(对于英国读者来说是地铁)走了!
您可以尝试其他类型的监管者,结果会一样。唯一的区别是重启策略
5> band_supervisor:start_link(angry). Musician Dorothy Frizzle, playing the singer entered the room Musician Arnold Li, playing the bass entered the room Musician Ralphie Perlstein, playing the drum entered the room Musician Carlos Perlstein, playing the keytar entered the room ... <snip> ... Ralphie Perlstein sucks! kicked that member out of the band! (drum) ... The manager is mad and fired the whole band! Carlos Perlstein just got back to playing in the subway
对于愤怒的监管者来说,当鼓手犯错时,鼓手和键盘手都被解雇了。这与混蛋的行为相比简直是小巫见大巫
6> band_supervisor:start_link(jerk). Musician Dorothy Franklin, playing the singer entered the room Musician Wanda Tennelli, playing the bass entered the room Musician Tim Perlstein, playing the drum entered the room Musician Dorothy Frizzle, playing the keytar entered the room ... <snip> ... Tim Perlstein played a false note. Uh oh Tim Perlstein sucks! kicked that member out of the band! (drum) The manager is mad and fired the whole band! Dorothy Franklin just got back to playing in the subway The manager is mad and fired the whole band! Wanda Tennelli just got back to playing in the subway The manager is mad and fired the whole band! Dorothy Frizzle just got back to playing in the subway
对于非动态的重启策略来说,这几乎就是全部。
动态监管
到目前为止,我们看到的监管类型都是静态的。我们在源代码中指定了所有我们将要拥有的子进程,并在之后让一切运行起来。这通常是大多数监管者在现实世界应用程序中最终的设置方式;它们通常用于监管架构组件。另一方面,您拥有监管不确定工作进程的监管者。它们通常是按需使用的。想想一个 web 服务器,它会为接收的每个连接生成一个进程。在这种情况下,您可能希望动态监管者检查所有将要拥有的不同进程。
每次使用one_for_one
、rest_for_one
或one_for_all
策略将工作进程添加到监管者时,子进程规范都会与 PID 和一些其他信息一起添加到监管者中的一个列表中。然后,子进程规范可用于重启子进程等等。由于事物以这种方式运作,因此存在以下接口
- start_child(SupervisorNameOrPid, ChildSpec)
- 这将子进程规范添加到列表中,并使用它启动子进程
- terminate_child(SupervisorNameOrPid, ChildId)
- 终止或暴力杀死子进程。子进程规范保留在监管者中
- restart_child(SupervisorNameOrPid, ChildId)
- 使用子进程规范让事情开始运转。
- delete_child(SupervisorNameOrPid, ChildId)
- 摆脱指定子进程的 ChildSpec
- check_childspecs([ChildSpec])
- 确保子进程规范有效。您可以在使用“start_child/2”之前使用它进行尝试。
- count_children(SupervisorNameOrPid)
- 计算监管者下的所有子进程,并为您提供一个比较列表,列出哪些子进程处于活动状态、有多少规范、有多少是监管者以及有多少是工作进程。
- which_children(SupervisorNameOrPid)
- 为您提供监管者下所有子进程的列表。
让我们看看它如何在音乐家中工作,输出已移除(您需要快速行动,才能超过失败的鼓手!)
1> band_supervisor:start_link(lenient). {ok,0.709.0>} 2> supervisor:which_children(band_supervisor). [{keytar,<0.713.0>,worker,[musicians]}, {drum,<0.715.0>,worker,[musicians]}, {bass,<0.711.0>,worker,[musicians]}, {singer,<0.710.0>,worker,[musicians]}] 3> supervisor:terminate_child(band_supervisor, drum). ok 4> supervisor:terminate_child(band_supervisor, singer). ok 5> supervisor:restart_child(band_supervisor, singer). {ok,<0.730.0>} 6> supervisor:count_children(band_supervisor). [{specs,4},{active,3},{supervisors,0},{workers,4}] 7> supervisor:delete_child(band_supervisor, drum). ok 8> supervisor:restart_child(band_supervisor, drum). {error,not_found} 9> supervisor:count_children(band_supervisor). [{specs,3},{active,3},{supervisors,0},{workers,3}]
您可以看到如何动态管理子进程。这对任何需要管理的动态事物(我想要启动这个,终止它等等)且数量较少的事物都非常有效。由于内部表示是一个列表,因此当您需要快速访问许多子进程时,这种方法效果不佳。
在这种情况下,您需要的是 simple_one_for_one
。simple_one_for_one
的问题在于它不允许您手动重启、删除或终止子进程。幸运的是,这种灵活性的损失伴随着一些优点。所有子进程都保存在一个字典中,这使得查找它们的速度很快。此外,所有子进程都使用同一个子进程规格。这将节省您的内存和时间,因为您将永远不需要自己删除子进程或存储任何子进程规格。
在大多数情况下,编写 simple_one_for_one
监督器与编写其他类型的监督器类似,只有一个例外。{M,F,A}
元组中的参数列表不是全部内容,它将在您调用 supervisor:start_child(Sup, Args)
时追加到您调用的内容。没错,supervisor:start_child/2
改变了 API。因此,与其像以前那样使用 supervisor:start_child(Sup, Spec)
,它会调用 erlang:apply(M,F,A)
,我们现在可以使用 supervisor:start_child(Sup, Args)
,它会调用 erlang:apply(M,F,A++Args)
。
以下是如何为我们的 band_supervisor 编写它。只需在其中某个地方添加以下子句即可
init(jamband) -> {ok, {{simple_one_for_one, 3, 60}, [{jam_musician, {musicians, start_link, []}, temporary, 1000, worker, [musicians]} ]}};
在本例中,我将它们全部设置为临时进程,并且监督器非常宽容
1> supervisor:start_child(band_supervisor, [djembe, good]). Musician Janet Tennelli, playing the djembe entered the room {ok,<0.690.0>} 2> supervisor:start_child(band_supervisor, [djembe, good]). {error,{already_started,<0.690.0>}}
糟糕!出现这种情况是因为我们将 djembe 演奏者注册为 djembe
,这是我们对 gen_server
的启动调用的一个部分。如果我们没有为它们命名或为每个演奏者使用不同的名称,就不会出现问题。实际上,以下示例使用 drum
作为名称
3> supervisor:start_child(band_supervisor, [drum, good]). Musician Arnold Ramon, playing the drum entered the room {ok,<0.696.0>} 3> supervisor:start_child(band_supervisor, [guitar, good]). Musician Wanda Perlstein, playing the guitar entered the room {ok,<0.698.0>} 4> supervisor:terminate_child(band_supervisor, djembe). {error,simple_one_for_one}
没错。正如我所说,无法以这种方式控制子进程。
5> musicians:stop(drum). Arnold Ramon left the room (drum) ok
这样效果更好。
作为一个通用的(有时错误的)提示,我会建议您仅在确信您将有很少的子进程需要监督,或者它们不需要以任何速度和频率进行操作时,才使用标准监督器。对于其他类型的动态监督,尽可能使用 simple_one_for_one
。
更新
从 R14B03 版本开始,可以使用 supervisor:terminate_child(SupRef, Pid)
函数终止子进程。简单的一对一监督方案现在可以完全动态化,并且在您拥有许多运行单一类型进程的进程时,已成为一种全面的选择。
关于监督策略和子进程规范就介绍到这里。现在您可能在想“我该如何从这些东西中构建一个可用的应用程序?”,如果您是这样想的,那么您会很高兴地进入下一章,它实际上构建了一个带有简短监督树的简单应用程序,以了解如何在现实世界中实现它。