在GenServer.start_link/3中使用{:via, module, term}注册名称的好处是什么?

16

GenServer.start_link/3 中,我可以像这样使用原子名称本地注册一个进程:

defmodule Worker do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, nil, name: :worker)
  end
end

然后我可以启动一个supervisor来监督这个进程:

defmodule Boss do
  use Supervisor

  def init(_) do
    children = [worker(Worker, [])]
    supervise(children, strategy: :one_for_one)
  end
end

现在我想让监管者监督3个“工人”进程,因此我需要为这些3个进程赋予唯一的名称,这样当监管者重新启动该进程时,它将始终使用相同的符号名称。
我可以简单地使用字符串插值来创建唯一的“工人”进程名称,例如:
defmodule Worker do
  use GenServer

  def start_link(id) do
    GenServer.start_link(__MODULE__, nil, name: :"worker_#{id}")
  end
end

然后监督这样的3个进程:

defmodule Boss do
  use Supervisor

  def init(_) do
    children = for id <- 1..3 do
      worker(Worker, [id], id: id)
    end
    supervise(children, strategy: :one_for_one)
  end
end

它像预期的那样工作。

在GenServer文档的“名称注册”部分下,它说你也可以使用{:via,module,term}来注册名称。

{:via,module,term} - 使用给定的机制和名称注册GenServer。 :via选项需要一个导出register_name /2 unregister_name /1 whereis_name /1 send /2 的模块。一个这样的例子是:global模块,它使用这些函数为Elixir节点网络中全局可用的进程的名称列表和关联的PID保留这些函数。Elixir还附带了一个本地的、去中心化的、可扩展的Registry注册表,用于动态生成并存储名称。

然而,要使用:via选项,您必须实现一个导出register_name /2 unregister_name /1 whereis_name /1 send /2 的模块,这似乎比上面显示的简单使用字符串插值技术困难得多。

所以我的问题是:

  1. 使用{:via,module,term}注册名称的好处是什么,而不仅仅是使用字符串插值?
  2. 是否有使用:via选项注册名称的实用示例?

4个回答

8

简而言之,:via 的作用是允许你使用非标准的进程注册库。它们必须符合接口要求(类似于在 Java 中实现接口),并可以提供额外的功能。

主要的例子是当你想要使用非标准的名称注册库时。以 gproc 库 为例。它遵循使用 :via 的接口要求,因此最小化地干扰了你的应用程序代码。此外,它提供了几个优势,超过了标准名称注册系统:

  1. 使用任何术语作为进程别名
  2. 在多个别名下注册进程
  3. 非唯一属性可以被许多进程同时注册;查询级理解(QLC) 和匹配规范接口用于字典上的高效查询
  4. 等待注册,让你等待直到一个进程注册自己
  5. 原子地将注册的名称和属性转让给另一个进程
  6. 计数器和聚合计数器,自动维护给定名称的所有计数器的总数
  7. 全局注册表,应用于网络节点的所有上述功能

Elixir 的 Registry 模块是另一个需要使用 via 元组的例子。


4
一个场景是,当您想动态地分配名称给工作进程时(也许它们由一个 DynamicSupervisor 或者一个 simple_one_for_one 监督器监督,并且是随着时间的推移动态创建的)。
因为原子永远不会被垃圾回收,您不能将它们用作这些工作进程的名称,否则您将不得不处理内存泄漏。
出于某种原因,在:global 模块上注册名称让我感到不舒服,因为全局状态经常被认为是有害的,特别是在高并发环境中(这就是为什么您选择 Erlang/Elixir 的原因)。
所以在这种情况下,最好在“命名空间”中注册名称。在这种情况下,{:via, module, term} 变体非常出色。其中module 用作命名空间,term 可以是任何东西(通常是字符串,因为易于理解,并且可以被垃圾回收)。
顺便说一句,如果您不想自己实现注册表,已经有一个模块Registry专门用于此目的。您只需要给注册表进程一个名称并对其进行监督即可。

2
使用:via元组可以很好地封装别名处理,并为您提供一个固定点,您可以在其中发现进程。此外,:via元组可以是任意复杂的,例如像{:my_worker, 1}这样的元组通常比搞弄字符串操作更容易使用。
(请注意,我正在学习Elixir,因此不要完全相信我的话。此外,可能存在更强大/更好的:via元组的论据。)

0

我也有同样的问题。我能想到两个原因,为什么你想使用 Registry 模块而不是生成一个动态名称,例如 :"worker:#{id}"

  • 原子不会被垃圾回收
  • 根据 Registry 文档: "注册表中的每个条目都与注册了该键的进程相关联。如果进程崩溃,则自动删除与该进程关联的键。"

因此,似乎注册表链接到其中的进程,并且如果这些进程失败,将删除条目:

iex(6)> {:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest2)
{:ok, #PID<0.445.0>}
iex(7)> name = {:via, Registry, {Registry.ViaTest2, "agent"}}
{:via, Registry, {Registry.ViaTest2, "agent"}}
iex(8)> {:ok, agent} = Agent.start(fn -> 0 end, name: name)
{:ok, #PID<0.449.0>}
iex(9)> Registry.lookup(Registry.ViaTest2, "agent")
[{#PID<0.449.0>, nil}]
iex(10)> Process.alive?(agent)
true
iex(11)> Process.exit(agent, :kill)
true
iex(12)> Process.alive?(agent)
false
iex(13)> Registry.lookup(Registry.ViaTest2, "agent")
[]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接