注册 | Registry

本地、分散和可伸缩的键值进程存储。

它允许开发人员使用给定的键查找一个或多个进程。如果注册表:unique键,一个键指向0或1个进程。如果注册表允许:duplicate键,一个键可以指向任意数量的进程。在这两种情况下,不同的键可以识别相同的进程。

注册表中的每个条目都与已注册密钥的进程关联。如果进程崩溃,则与该进程关联的键会自动删除。注册表中的所有关键字比较都是使用匹配操作(===)完成的。

注册表可用于不同的目的,例如名称查找(使用:via选项),存储属性,自定义调度规则或pubsub实现。我们探讨下面的一些用例。

注册表还可以透明地分区,这为在具有数千或数百万项的高度并发环境中运行注册中心提供了更可伸缩的行为。

使用 :via

一旦注册中心以给定的名称启动后,将使用Registry.start_link/1,则可以使用{:via, Registry, {registry, key}}元组:

{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest)
name = {:via, Registry, {Registry.ViaTest, "agent"}}
{:ok, _} = Agent.start_link(fn -> 0 end, name: name)
Agent.get(name, & &1)
#=> 0
Agent.update(name, & &1 + 1)
Agent.get(name, & &1)
#=> 1

通常,注册表是作为监督树的一部分启动的:

{Registry, keys: :unique, name: Registry.ViaTest}

只能使用具有唯一密钥的注册表:via。如果该名称已被采用,则会返回特定于案例的start_link功能(Agent.start_link/2在上例中){:error, {:already_started, current_pid}}

用作调度员

Registry具有一种调度机制,允许开发人员实现从调用方触发的自定义调度逻辑。例如,假设我们已经启动了一个重复的注册表,如下所示:

{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.DispatcherTest)

通过调用register/3,不同的进程可以在给定的键下注册,并将该键下的任何值关联起来。在这种情况下,让我们在键下注册当前进程"hello"并将{IO, :inspect}元组附加到它:

{:ok, _} = Registry.register(Registry.DispatcherTest, "hello", {IO, :inspect})

现在,有兴趣为给定密钥分派事件的实体可能会调用dispatch/3传入密钥和回调。这个回调将被调用,其中列出了所请求的键下注册的所有值,以及以{pid, value}元组形式表示的注册每个值的进程的PID 。在我们的例子中,value将会是{module, function}上面代码中的元组:

Registry.dispatch(Registry.DispatcherTest, "hello", fn entries ->
  for {pid, {module, function}} <- entries, do: apply(module, function, [pid])
end)
# Prints #PID<...> where the pid is for the process that called register/3 above
#=> :ok

调度发生在dispatch/3多个分区的情况下(通过派生任务)串行调用或并行调用的进程中。注册过程不参与调度,除非涉及明确完成(例如,通过在回调中发送消息)。

此外,如果调度失败,由于注册错误,调度将始终失败,注册进程将不会被通知。因此,让我们确保至少包装并报告这些错误:

require Logger
Registry.dispatch(Registry.DispatcherTest, "hello", fn entries ->
  for {pid, {module, function}} <- entries do
    try do
      apply(module, function, [pid])
    catch
      kind, reason ->
        formatted = Exception.format(kind, reason, System.stacktrace)
        Logger.error "Registry.dispatch/3 failed with #{formatted}"
    end
  end
end)
# Prints #PID<...>
#=> :ok

你也可以替换整个apply系统通过显式发送消息。这就是我们接下来会看到的例子。

作为PubSub使用

注册中心还可以通过依赖于dispatch/3函数,类似于上一节:但是,在本例中,我们将向每个关联进程发送消息,而不是调用给定的模块函数。

在本例中,我们还将分区的数量设置为联机调度器的数量,这将使注册表在高度并发的环境中具有更高的性能:

{:ok, _} = Registry.start_link(keys: :duplicate, name: Registry.PubSubTest,
                               partitions: System.schedulers_online)
{:ok, _} = Registry.register(Registry.PubSubTest, "hello", [])
Registry.dispatch(Registry.PubSubTest, "hello", fn entries ->
  for {pid, _} <- entries, do: send(pid, {:broadcast, "world"})
end)
#=> :ok

上面的例子将消息广播{:broadcast, "world"}给所有在“topic”(或者“key”)下注册的进程,我们称之为直到现在)"hello"

给出的第三个参数register/3是与当前进程相关的值。在上一节我们在调度时使用它,在这个特殊的例子中我们对它没有兴趣,所以我们将它设置为一个空的列表。如有必要,您可以存储更有意义的值。

注册

查找,调度和注册是有效的和立即以延迟取消订阅为代价。例如,如果某个进程崩溃,其密钥会自动从注册表中删除,但该更改可能不会立即传播。这意味着某些操作可能会返回已经死亡的进程。如果发生这种情况,将在功能文档中明确说明。

但是,请记住,这些案件通常不是一个问题。毕竟,PID引用的进程可能随时崩溃,包括从注册表获取值和发送消息之间。标准库的许多部分都是为处理这些问题而设计的,例如Process.monitor/1它将提供:DOWN消息,如果被监视的进程已经死亡,并且Kernel.send/2对死过程起着不操作的作用。

ETS

请注意,注册表每个分区使用一个ETS表和两个ETS表。

类型

键()

注册时允许的密钥类型

键()

注册表的类型

meta_key()

注册表元数据项的类型

meta_value()

注册表元数据值的类型

注册表()

注册表标识符

值()

注册时允许的值类型。

功能

调度(注册表,键,mfa_or_fun,opts \ [])

调用具有以下所有条目的回调key在给定的每个分区中registry

键(注册表,pid)

返回给定的pidregistry没有特别的顺序

查找(注册表,键)

发现{pid, value}对给定的keyregistry没有特别的顺序

匹配(注册表,键,模式,警卫\ [])

回报{pid, value}给定下对keyregistry那场比赛pattern

元(注册表,键)

读取给出的注册表元数据start_link/3

put_meta(注册表,键值)

存储注册表元数据

注册(注册表,键,值)

下注册当前进程。keyregistry

START_LINK(选项)

将注册表作为管理程序启动。

start_link(键,名称,选项\ [])

将注册表作为管理程序启动。

取消注册(注册表,键)

取消注册给定的所有条目。key与当前进程关联的registry

unregister_match(注册表,键,模式,警卫\ [])

取消匹配模式的给定键的寄存器条目

update_value(注册表,键,回调)

更新key对于当前进程中的唯一registry

键()

key() :: term

注册时允许的密钥类型

键()

keys() :: :unique | :duplicate

注册表的类型

meta_key()

meta_key() :: atom | tuple

注册表元数据项的类型

meta_value()

meta_value() :: term

注册表元数据值的类型

注册表()

registry() :: atom

注册表标识符

值()

value() :: term

注册时允许的值类型。

调度(注册表,键,mfa_or_fun,opts \ [])

dispatch(registry, key, (entries :: [{pid, value}] -> term), keyword) :: :ok

key给定的每个分区下的所有条目调用回调registry

表表entries是由两个元素元组组成的非空列表,其中第一个元素是PID,第二个元素是与PID关联的值。如果没有给定键的条目,则永远不会调用回调。

如果注册表已分区,则每个分区将多次调用回调。如果注册表被分区并parallel: true作为选项提供,则调度将并行进行。在这两种情况下,只有当该分区有条目时才会调用回调。

有关使用dispatch/3用于建立自定义调度或公共子系统的功能。

键(注册表,pid)

keys(registry, pid) :: [key]

返回给定的pidregistry没有特别的顺序。

如果注册表是唯一的,则键是唯一的。否则,如果进程在同一密钥下多次注册,则它们可能包含重复项。如果进程已死或该进程在此注册表中没有项,则列表将为空。

实例

在唯一注册表下注册不允许多个条目:

iex> Registry.start_link(:unique, Registry.UniqueKeysTest)
iex> Registry.keys(Registry.UniqueKeysTest, self())
[]
iex> {:ok, _} = Registry.register(Registry.UniqueKeysTest, "hello", :world)
iex> Registry.register(Registry.UniqueKeysTest, "hello", :later) # registry is :unique
{:error, {:already_registered, self()}}
iex> Registry.keys(Registry.UniqueKeysTest, self())
["hello"]

不过,重复登记也有可能:

iex> Registry.start_link(:duplicate, Registry.DuplicateKeysTest)
iex> Registry.keys(Registry.DuplicateKeysTest, self())
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateKeysTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.DuplicateKeysTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateKeysTest, self())
["hello", "hello"]

查找(注册表,键)

lookup(registry, key) :: [{pid, value}]

发现{pid, value}对给定的keyregistry没有特别的顺序。

如果没有匹配,则为空列表。

对于唯一的注册中心,一个分区查找是必要的。对于重复的注册表,必须查找所有分区。

实例

在下面的示例中,我们注册了当前流程,并从它本身和其他进程中查找它:

iex> Registry.start_link(:unique, Registry.UniqueLookupTest)
iex> Registry.lookup(Registry.UniqueLookupTest, "hello")
[]
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
iex> Registry.lookup(Registry.UniqueLookupTest, "hello")
[{self(), :world}]
iex> Task.async(fn -> Registry.lookup(Registry.UniqueLookupTest, "hello") end) |> Task.await
[{self(), :world}]

同样适用于重复的登记册:

iex> Registry.start_link(:duplicate, Registry.DuplicateLookupTest)
iex> Registry.lookup(Registry.DuplicateLookupTest, "hello")
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
iex> Registry.lookup(Registry.DuplicateLookupTest, "hello")
[{self(), :world}]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
iex> Enum.sort(Registry.lookup(Registry.DuplicateLookupTest, "hello"))
[{self(), :another}, {self(), :world}]

匹配(注册表,键,模式,警卫\ [])

match(registry, key, match_pattern :: atom | tuple, guards :: list) :: [{pid, term}]

回报{pid, value}给定下对keyregistry那场比赛pattern...

模式必须是与存储在注册表中的值的结构相匹配的原子或元组。原子:_可以用于忽略给定值或元组元素,而“$1”可用于暂时将部分模式赋值给变量以进行后续比较。

可以通过警戒条件列表进行更精确的匹配。每个守卫都是一个元组,它描述了应该由指定的模式部分传递的检查。例如:“$ 1”> 1守卫条件将表示为{:>,:“$ 1”,1}元组。请注意,守卫条件只适用于指定的变量,如“$ 1”,:“$ 2”等。避免使用特殊匹配变量:“$ _”和:“$$”,因为它可能无法按预期工作。

如果没有匹配,将返回空列表。

对于唯一的注册中心,一个分区查找是必要的。对于重复的注册表,必须查找所有分区。

实例

在下面的示例中,我们在重复注册表中的同一项下注册当前进程,但值不同:

iex> Registry.start_link(:duplicate, Registry.MatchTest)
iex> {:ok, _} = Registry.register(Registry.MatchTest, "hello", {1, :atom, 1})
iex> {:ok, _} = Registry.register(Registry.MatchTest, "hello", {2, :atom, 2})
iex> Registry.match(Registry.MatchTest, "hello", {1, :_, :_})
[{self(), {1, :atom, 1}}]
iex> Registry.match(Registry.MatchTest, "hello", {2, :_, :_})
[{self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :atom, :_}) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:"$1", :_, :"$1"}) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}])
[{self(), {2, :atom, 2}}]
iex> Registry.match(Registry.MatchTest, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) |> Enum.sort()
[{self(), {1, :atom, 1}}, {self(), {2, :atom, 2}}]

元(注册表,键)

meta(registry, meta_key) :: {:ok, meta_value} | :error

读取给出的注册表元数据start_link/3...

原子和元组被允许作为键。

实例

iex> Registry.start_link(:unique, Registry.MetaTest, meta: [custom_key: "custom_value"])
iex> Registry.meta(Registry.MetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.meta(Registry.MetaTest, :unknown_key)
:error

put_meta(注册表,键值)

put_meta(registry, meta_key, meta_value) :: :ok

存储注册表元数据。

原子和元组被允许作为键。

实例

iex> Registry.start_link(:unique, Registry.PutMetaTest)
iex> Registry.put_meta(Registry.PutMetaTest, :custom_key, "custom_value")
:ok
iex> Registry.meta(Registry.PutMetaTest, :custom_key)
{:ok, "custom_value"}
iex> Registry.put_meta(Registry.PutMetaTest, {:tuple, :key}, "tuple_value")
:ok
iex> Registry.meta(Registry.PutMetaTest, {:tuple, :key})
{:ok, "tuple_value"}

注册(注册表,键,值)

register(registry, key, value) ::
  {:ok, pid} |
  {:error, {:already_registered, pid}}

下注册当前进程。keyregistry...

还必须给出与此注册相关的值。每当分派或执行密钥查找时,都会检索此值。

此函数返回{:ok, owner}{:error, reason}...owner是注册表分区中负责PID的PID。所有者自动链接到调用方。

如果注册表有唯一的项,它将返回{:ok, owner}除非键已经与PID相关联,否则它将返回{:error, {:already_registered, pid}}...

如果注册表有重复的项,则允许在同一项下从当前进程多次注册。

实例

在唯一注册表下注册不允许多个条目:

iex> Registry.start_link(:unique, Registry.UniqueRegisterTest)
iex> {:ok, _} = Registry.register(Registry.UniqueRegisterTest, "hello", :world)
iex> Registry.register(Registry.UniqueRegisterTest, "hello", :later)
{:error, {:already_registered, self()}}
iex> Registry.keys(Registry.UniqueRegisterTest, self())
["hello"]

不过,重复登记也有可能:

iex> Registry.start_link(:duplicate, Registry.DuplicateRegisterTest)
iex> {:ok, _} = Registry.register(Registry.DuplicateRegisterTest, "hello", :world)
iex> {:ok, _} = Registry.register(Registry.DuplicateRegisterTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateRegisterTest, self())
["hello", "hello"]

START_LINK(选项)

start_link(keys: keys, name: registry, partitions: pos_integer, listeners: [atom], meta: [{meta_key, meta_value}]) ::
  {:ok, pid} |
  {:error, term}

将注册表作为管理程序启动。

它可以手动启动,如下所示:

Registry.start_link(keys: :unique, name: MyApp.Registry)

在你的主管树中,你会写:

Supervisor.start_link([
  {Registry, keys: :unique, name: MyApp.Registry}
])

对于密集型工作负载,还可以对注册表进行分区(通过指定:partitions选项)。如果需要分区,那么很好的默认设置是将分区数量设置为可用的调度程序数量:

Registry.start_link(keys: :unique, name: MyApp.Registry,
                    partitions: System.schedulers_online())

或:

Supervisor.start_link([
  {Registry, keys: :unique, name: MyApp.Registry,
             partitions: System.schedulers_online()}
])

备选方案

注册表需要下列项:

  • :keys-选择是否有钥匙:unique:duplicate
  • :name-登记册及其表的名称

以下键是可选的:

  • :partitions-注册表中的分区数。默认为1...
  • :listeners-已通知的已命名进程的列表:register:unregister事件。如果侦听器想要在注册进程崩溃时得到通知,则必须由侦听器监视已注册的进程。
  • :meta-要附加到登记册的元数据关键字清单。

start_link(键,名称,选项\ [])

start_link(keys, registry, keyword) ::
  {:ok, pid} |
  {:error, term}

将注册表作为管理程序启动。

类似于start_link/1除了必要的选择,keysname作为论据。

取消注册(注册表,键)

unregister(registry, key) :: :ok

取消注册给定的所有条目。key与当前进程关联的registry...

:ok如果没有更多关键字与当前进程关联,则始终返回:ok并自动取消当前进程与所有者的链接。另请参阅register/3详细了解“所有者”。

实例

对于独特的登记册:

iex> Registry.start_link(:unique, Registry.UniqueUnregisterTest)
iex> Registry.register(Registry.UniqueUnregisterTest, "hello", :world)
iex> Registry.keys(Registry.UniqueUnregisterTest, self())
["hello"]
iex> Registry.unregister(Registry.UniqueUnregisterTest, "hello")
:ok
iex> Registry.keys(Registry.UniqueUnregisterTest, self())
[]

重复登记册:

iex> Registry.start_link(:duplicate, Registry.DuplicateUnregisterTest)
iex> Registry.register(Registry.DuplicateUnregisterTest, "hello", :world)
iex> Registry.register(Registry.DuplicateUnregisterTest, "hello", :world)
iex> Registry.keys(Registry.DuplicateUnregisterTest, self())
["hello", "hello"]
iex> Registry.unregister(Registry.DuplicateUnregisterTest, "hello")
:ok
iex> Registry.keys(Registry.DuplicateUnregisterTest, self())
[]

unregister_match(注册表,键,模式,警卫\ [])

取消匹配模式的给定键的项。

实例

对于唯一的注册中心,可以根据其是否与特定值匹配来有条件地注销密钥。

iex> Registry.start_link(:unique, Registry.UniqueUnregisterMatchTest)
iex> Registry.register(Registry.UniqueUnregisterMatchTest, "hello", :world)
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :foo)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
["hello"]
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :world)
:ok
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
[]

重复登记册:

iex> Registry.start_link(:duplicate, Registry.DuplicateUnregisterMatchTest)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_b)
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_c)
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello", "hello"]
iex> Registry.unregister_match(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
:ok
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
["hello", "hello"]
iex> Registry.lookup(Registry.DuplicateUnregisterMatchTest, "hello")
[{self(), :world_b}, {self(), :world_c}]

update_value(注册表,键,回调)

update_value(registry, key, (value -> value)) ::
  {new_value :: term, old_value :: term} |
  :error

更新key对于当前进程中的唯一registry...

返回{new_value, old_value}元组或:error如果没有分配给当前进程的密钥。

如果给定非唯一注册表,则会引发错误。

实例

iex> Registry.start_link(:unique, Registry.UpdateTest)
iex> {:ok, _} = Registry.register(Registry.UpdateTest, "hello", 1)
iex> Registry.lookup(Registry.UpdateTest, "hello")
[{self(), 1}]
iex> Registry.update_value(Registry.UpdateTest, "hello", & &1 + 1)
{2, 1}
iex> Registry.lookup(Registry.UpdateTest, "hello")
[{self(), 2}]

扫码关注云+社区

领取腾讯云代金券