Elixir语言中 Supervisor 相关用法介绍如下。
用于实现监督者的行为模块。
监督者是监督其他进程的进程,我们称之为child processes。主管用于构建称为 supervision tree 的分层流程结构。监督树提供fault-tolerance 并封装我们的应用程序如何启动和关闭。
主管可以通过  直接使用子列表启动,或者您可以定义一个基于模块的主管来实现所需的回调。下面的部分在大多数示例中使用start_link/2   来启动supervisor,但它也包括一个关于基于模块的特定部分。start_link/2 
例子
为了启动监督者,我们需要首先定义一个将被监督的子进程。作为示例,我们将定义一个表示堆栈的GenServer:
defmodule Stack do
  use GenServer
  def start_link(state) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end
  ## Callbacks
  @impl true
  def init(stack) do
    {:ok, stack}
  end
  @impl true
  def handle_call(:pop, _from, [head | tail]) do
    {:reply, head, tail}
  end
  @impl true
  def handle_cast({:push, head}, tail) do
    {:noreply, [head | tail]}
  end
end
堆栈是列表的一个小包装器。它允许我们将一个元素放在栈顶,通过预先添加到列表中,并通过模式匹配获得栈顶。
我们现在可以启动一个主管,它将启动并监督我们的堆栈过程。第一步是定义一个子规范列表,以控制每个子规范的行为方式。每个子规范都是一个映射,如下所示:
children = [
  # The Stack is a child started via Stack.start_link([:hello])
  %{
    id: Stack,
    start: {Stack, :start_link, [[:hello]]}
  }
]
# Now we start the supervisor with the children and a strategy
{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
# After started, we can query the supervisor for information
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
请注意,在启动 GenServer 时,我们使用名称 Stack 注册它,这允许我们直接调用它并获取堆栈中的内容:
GenServer.call(Stack, :pop)
#=> :hello
GenServer.cast(Stack, {:push, :world})
#=> :ok
GenServer.call(Stack, :pop)
#=> :world
但是,我们的堆栈服务器中有一个错误。如果我们调用 :pop 并且堆栈为空,它将崩溃,因为没有子句匹配:
GenServer.call(Stack, :pop)
** (exit) exited in: GenServer.call(Stack, :pop, 5000)
幸运的是,由于服务器由主管监督,主管将自动启动一个新的,初始堆栈为 [:hello] :
GenServer.call(Stack, :pop)
#=> :hello
主管支持不同的策略;在上面的示例中,我们选择了 :one_for_one 。此外,每个主管可以有许多工人和/或主管作为孩子,每个人都有自己的配置(如"Child specification" 部分所述)。
本文档的其余部分将介绍如何指定子进程、如何启动和停止它们、不同的监督策略等等。
子规格
子规范说明了主管如何启动、关闭和重新启动子进程。
子规范是一个最多包含 6 个元素的映射。以下列表中的前两个键是必需的,其余的是可选的:
- 
:id- 主管内部用于标识子规范的任何术语;默认为给定的模块。在:id值冲突的情况下,主管将拒绝初始化并要求明确的 ID。此 key 是必需的。 - 
:start- 带有 module-function-args 的元组将被调用以启动子进程。此 key 是必需的。 - 
:restart- 定义终止子进程何时应该重新启动的原子(参见下面的"Restart values" 部分)。此键是可选的,默认为:permanent。 - 
:shutdown- 定义子进程应如何终止的整数或原子(请参阅下面的 "Shutdown values" 部分)。此键是可选的,如果类型为:worker则默认为5_000,如果类型为:supervisor则默认为:infinity。 - 
:type- 指定子进程是:worker或:supervisor。此键是可选的,默认为:worker。 
还有第六个键 :modules ,它是可选的,很少更改。它是根据:start 值自动设置的。
让我们了解:shutdown 和:restart 选项控制的内容。
关机值 (:shutdown)
:shutdown 选项支持以下关闭值:
- 
:brutal_kill- 使用Process.exit(child, :kill)无条件立即终止子进程。 - 
任何整数 >= 0 - 主管在发出
Process.exit(child, :shutdown)信号后等待其子级终止的时间量(以毫秒为单位)。如果子进程没有捕获退出,初始的:shutdown信号将立即终止子进程。如果子进程正在捕获退出,它有给定的时间来终止。如果它没有在指定时间内终止,则子进程将由主管通过Process.exit(child, :kill)无条件终止。 - 
:infinity- 作为整数工作,但主管将无限期地等待子进程终止。如果子进程是主管,则推荐值为:infinity,以给主管及其子进程足够的时间关闭。此选项可用于普通工人,但不鼓励这样做并且需要格外小心。如果不小心使用,子进程将永远不会终止,从而阻止您的应用程序也终止。 
重启值 (:restart)
:restart 选项控制主管应该认为什么是成功终止。如果终止成功,主管将不会重新启动子进程。如果子进程崩溃,主管将启动一个新进程。
:restart 选项支持以下重启值:
- 
:permanent- 子进程总是重新启动。 - 
:temporary- 无论监督策略如何,子进程都不会重新启动:任何终止(甚至异常)都被认为是成功的。 - 
:transient- 仅当子进程异常终止时才会重新启动,即,退出原因不是:normal、:shutdown或{:shutdown, term}。 
要更全面地了解退出原因及其影响,请参阅“退出原因和重新启动”部分。
child_spec/1
在启动主管时,我们传递一个子规范列表。这些规范是说明主管应该如何启动、停止和重新启动其每个子项的映射:
%{
  id: Stack,
  start: {Stack, :start_link, [[:hello]]}
}
上面的映射定义了一个具有 Stack 的 :id 的子节点,该子节点通过调用 Stack.start_link([:hello]) 来启动。
但是,将每个子节点的子规范指定为映射可能很容易出错,因为我们可能会更改 Stack 实现并忘记更新其规范。这就是为什么 Elixir 允许您传递带有模块名称和 start_link 参数的元组而不是规范的原因:
children = [
  {Stack, [:hello]}
]
然后主管将调用Stack.child_spec([:hello]) 来检索子规范。现在Stack模块负责构建自己的规范,例如,我们可以这样写:
def child_spec(arg) do
  %{
    id: Stack,
    start: {Stack, :start_link, [arg]}
  }
end
幸运的是,use GenServer 已经定义了一个与上面完全相同的 Stack.child_spec/1。如果您需要自定义   ,可以将选项直接传递给 GenServer use GenServer :
use GenServer, restart: :transient
最后,请注意,也可以简单地将 Stack 模块作为子模块传递:
children = [
  Stack
]
仅给出模块名称时,它等效于 {Stack, []} 。通过将Map规范替换为 {Stack, [:hello]} 或 Stack ,我们使用 use GenServer 定义的默认实现将子规范封装在 Stack 模块中。我们现在可以与其他开发人员共享我们的Stack worker,他们可以直接将其添加到他们的监督树中,而不必担心 worker 的底层细节。
总体而言,子规范可以是以下之一:
- 表示子规范本身的映射 - 如 "Child specification" 部分所述
 - 一个元组,其中一个模块作为第一个元素,开始参数作为第二个元素 - 例如 
{Stack, [:hello]}。在这种情况下,调用Stack.child_spec([:hello])来检索子规范 - 一个模块 - 例如 
Stack。在这种情况下,调用Stack.child_spec([])来检索子规范 
如果需要将元组或模块子规范转换为映射或修改子规范,可以使用  函数。例如,要使用不同的 Supervisor.child_spec/2 :id 和 10 秒(10_000 毫秒)的 :shutdown 值运行堆栈:
children = [
  Supervisor.child_spec({Stack, [:hello]}, id: MyStack, shutdown: 10_000)
]
基于模块的主管
在上面的示例中,通过将监督结构传递给   来启动监督者。但是,也可以通过显式定义监督模块来创建监督者:start_link/2 
defmodule MyApp.Supervisor do
  # Automatically defines child_spec/1
  use Supervisor
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end
  @impl true
  def init(_init_arg) do
    children = [
      {Stack, [:hello]}
    ]
    Supervisor.init(children, strategy: :one_for_one)
  end
end
这两种方法之间的区别在于,基于模块的监督者可以让您更直接地控制监督者的初始化方式。我们不是使用自动初始化的子项列表调用 ,而是通过在其Supervisor.start_link/2   回调中调用init/1   来手动初始化子项。Supervisor.init/2 
use Supervisor 还定义了一个 child_spec/1 函数,它允许我们将 MyApp.Supervisor 作为另一个主管的孩子或在您的监督树的顶部运行:
children = [
  MyApp.Supervisor
]
Supervisor.start_link(children, strategy: :one_for_one)
一般准则是仅在监督树的顶部使用不带回调模块的监督者,通常在  回调中。我们建议对应用程序中的任何其他主管使用基于模块的主管,以便它们可以作为树中另一个主管的子级运行。 Application.start/2   自动生成的Supervisor child_spec/1 可以使用以下选项进行自定义:
:id- 子规范标识符,默认为当前模块:restart- 当应重新启动主管时,默认为:permanent
紧接在use Supervisor 之前的@doc 注释将附加到生成的child_spec/1 函数。
 start_link/2  、  init/2  和策略
start_link/2 init/2 到目前为止,我们已经启动了主管将单个孩子作为元组传递以及一个名为 :one_for_one 的策略:
children = [
  {Stack, [:hello]}
]
Supervisor.start_link(children, strategy: :one_for_one)
或从   回调内部:init/1 
children = [
  {Stack, [:hello]}
]
Supervisor.init(children, strategy: :one_for_one)
  和start_link/2   的第一个参数是上面"child_spec/1" 部分中定义的子规范列表。init/2 
第二个参数是选项的关键字列表:
- 
:strategy- 监督策略选项。它可以是:one_for_one、:rest_for_one或:one_for_all。必需的。请参阅"Strategies" 部分。 - 
:max_restarts- 在一个时间范围内允许的最大重启次数。默认为3。 - 
:max_seconds-:max_restarts适用的时间范围。默认为5。 - 
:name- 注册主管进程的名称。支持的值在文档的 "Name registration" 部分进行了说明。可选的。GenServer 
策略
监督者支持不同的监督策略(通过 :strategy 选项,如上所示):
- 
:one_for_one- 如果子进程终止,则仅重新启动该进程。 - 
:one_for_all- 如果子进程终止,则所有其他子进程都将终止,然后所有子进程(包括终止的子进程)将重新启动。 - 
:rest_for_one- 如果子进程终止,则终止的子进程和在它之后启动的其余子进程将终止并重新启动。 
上面的进程终止是指终止不成功,由:restart选项决定。
要动态监督孩子,请参阅 。DynamicSupervisor 
名称注册
主管与   绑定到相同的名称注册规则。在 GenServer   的文档中阅读有关这些规则的更多信息。GenServer 
启动和关闭
当主管启动时,它会遍历所有子规范,然后按照定义的顺序启动每个子规范。这是通过调用子规范中 :start 键下定义的函数来完成的,通常默认为 start_link/1 。
然后为每个子进程调用start_link/1(或自定义)。 start_link/1 函数必须返回 {:ok, pid} 其中 pid 是链接到主管的新进程的进程标识符。子进程通常通过执行  回调来开始其工作。一般来说,init/1 init回调是我们初始化和配置子进程的地方。
关闭过程以相反的顺序发生。
当主管关闭时,它会以与列出的相反顺序终止所有孩子。终止是通过 Process.exit(child_pid, :shutdown) 向子进程发送关闭退出信号,然后等待子进程终止的时间间隔来实现的。此时间间隔默认为 5000 毫秒。如果子进程在此时间间隔内没有终止,则主管突然终止子进程,原因是 :kill 。可以在子规范中配置关闭时间,这将在下一节中详细介绍。
如果子进程没有捕获退出,它会在收到第一个退出信号时立即关闭。如果子进程正在捕获退出,则调用terminate 回调,并且子进程必须在合理的时间间隔内终止,然后才被主管突然终止。
换句话说,如果一个进程在你的应用程序或监督树关闭时自行清理很重要,那么这个进程必须捕获出口,并且它的子规范应该指定正确的 :shutdown 值,确保它在合理的范围内终止间隔。
退出原因和重启
主管根据其:restart 配置重新启动子进程。例如,当 :restart 设置为 :transient 时,如果子进程因 :normal、:shutdown 或 {:shutdown, term} 原因退出,则主管不会重新启动子进程。
那么有人可能会问:退出的时候应该选择哪个退出原因呢?有三个选项:
- 
:normal- 在这种情况下,不会记录退出,在瞬态模式下不会重新启动,并且链接的进程不会退出 - 
:shutdown或{:shutdown, term}- 在这种情况下,不会记录退出,在瞬态模式下不会重新启动,并且链接进程以相同的原因退出,除非它们正在捕获退出 - 
任何其他术语-在这种情况下,将记录退出,在瞬态模式下重新启动,并且链接的进程以相同的原因退出,除非它们正在捕获退出
 
请注意,达到最大重启强度的主管将退出 :shutdown 原因。在这种情况下,只有在将 :restart 选项设置为 :permanent(默认值)的情况下定义其子规范时,才会重新启动主管。
相关用法
- Elixir Supervisor.Spec.worker用法及代码示例
 - Elixir Supervisor.child_spec用法及代码示例
 - Elixir Supervisor.init用法及代码示例
 - Elixir Supervisor.Spec用法及代码示例
 - Elixir Supervisor.Spec.supervise用法及代码示例
 - Elixir Supervisor.Spec.supervisor用法及代码示例
 - Elixir StringIO.flush用法及代码示例
 - Elixir Stream用法及代码示例
 - Elixir String.contains?用法及代码示例
 - Elixir String.reverse用法及代码示例
 - Elixir Stream.drop_while用法及代码示例
 - Elixir String.to_integer用法及代码示例
 - Elixir System.stop用法及代码示例
 - Elixir Stream.map_every用法及代码示例
 - Elixir Stream.iterate用法及代码示例
 - Elixir String.pad_trailing用法及代码示例
 - Elixir Stream.dedup_by用法及代码示例
 - Elixir Stream.interval用法及代码示例
 - Elixir String.split_at用法及代码示例
 - Elixir String.valid?用法及代码示例
 - Elixir String.replace_prefix用法及代码示例
 - Elixir String.printable?用法及代码示例
 - Elixir Stream.zip用法及代码示例
 - Elixir System.fetch_env!用法及代码示例
 - Elixir String.replace_trailing用法及代码示例
 
注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 Supervisor。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。
