代理 | Agent
代理人
本章是
Mix和OTP指南的一部分
,它取决于本指南的前几章。有关更多信息,请阅读简介指南或查看边栏中的章节索引。
在本章中,我们将创建一个名为的模块KV.Bucket
。该模块将负责存储我们的键值条目,使其能够被其他流程读取和修改。
如果您已经跳过入门指南或很久以前阅读,请务必重新阅读“流程”一章。我们将以此为起点。
状态的麻烦
Elixir是一种不可变的语言,默认情况下不会共享任何内容。如果我们想要提供可从多个地方读取和修改的桶,我们在Elixir中有两个主要选项:
- 流程
我们介绍了入门指南中的流程。ETS是一个将在后面的章节中探讨的新主题。当涉及到流程时,我们很少手动推出自己的流程,而是使用Elixir和OTP提供的抽象:
- 代理 - 简单的状态包装。
- GenServer - 封装状态,提供同步和异步调用,支持代码重新加载等的“通用服务器”(进程)。
- 任务 - 异步计算单元,允许生成一个进程并在稍后时间检索其结果。
我们将在本指南中探索大部分这些抽象概念。请记住,他们都在使用由VM,如提供的基本功能,流程上实现send
,receive
,spawn
和link
。
代理
代理是简单的状态包装。如果你想要从一个过程中保持状态,那么代理是非常合适的。让我们iex
在项目内部开始一个会话:
$ iex -S mix
和代理玩一下:
iex> {:ok, agent} = Agent.start_link fn -> [] end
{:ok, #PID<0.57.0>}
iex> Agent.update(agent, fn list -> ["eggs" | list] end)
:ok
iex> Agent.get(agent, fn list -> list end)
["eggs"]
iex> Agent.stop(agent)
:ok
我们启动了一个初始状态为空列表的代理。我们更新了代理人的状态,将我们的新项目添加到列表的头部。第二个参数Agent.update/3
是一个函数,它将代理的当前状态作为输入并返回其所需的新状态。最后,我们检索了整个列表。第二个参数Agent.get/3
是一个函数,它将状态作为输入并返回Agent.get/3
本身将返回的值。一旦我们完成了代理,我们可以调用Agent.stop/3
终止代理进程。
让我们KV.Bucket
来实现我们的使用代理。但在开始实施之前,我们先写一些测试。在下面创建一个文件test/kv/bucket_test.exs
(记住.exs
扩展名):
defmodule KV.BucketTest do
use ExUnit.Case, async: true
test "stores values by key" do
{:ok, bucket} = start_supervised KV.Bucket
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
end
我们的第一次测试开始一个新的KV.Bucket
使用start_supervised
功能,并执行一些get/2
和put/3
操作就可以了,断言结果。我们不需要明确地停止代理,因为我们使用了该代理,start_supervised/2
并且在测试完成时自动终止测试中的进程。
还要注意async: true
传递给的选项ExUnit.Case
。该选项:async
通过在我们的机器中使用多个核心,使测试用例与其他测试用例并行运行。这对加速我们的测试套件非常有用。但是,:async
必须只
如果测试情况下不依赖于或更改任何全局值进行设定。例如,如果测试需要写入文件系统或访问数据库,请保持同步(省略:async
选项)以避免测试之间的竞争状况。
异步与否,我们的新测试显然会失败,因为在被测试的模块中没有实现任何功能:
** (ArgumentError) The module KV.Bucket was given as a child to a supervisor but it does not implement child_spec/1
由于模块尚未定义,因此child_spec/1
尚不存在。
为了修复失败的测试,让我们lib/kv/bucket.ex
用下面的内容创建一个文件。KV.Bucket
在下面的实现中窥视之前,请随时尝试使用代理自己实现模块。
defmodule KV.Bucket do
use Agent
@doc """
Starts a new bucket.
"""
def start_link(_opts) do
Agent.start_link(fn -> %{} end)
end
@doc """
Gets a value from the `bucket` by `key`.
"""
def get(bucket, key) do
Agent.get(bucket, &Map.get(&1, key))
end
@doc """
Puts the `value` for the given `key` in the `bucket`.
"""
def put(bucket, key, value) do
Agent.update(bucket, &Map.put(&1, key, value))
end
end
我们实施的第一步是打电话use Agent
。通过这样做,它将定义一个child_spec/1
包含开始我们的过程的确切步骤的函数。
然后我们定义一个start_link/1
函数,它将有效地启动代理。该start_link/1
函数总是收到一个选项列表,但我们现在不打算使用它。然后我们继续调用Agent.start_link/1
,它接收一个返回代理初始状态的匿名函数。
我们在代理内部保存地图来存储我们的密钥和值。&
通过入门指南中介绍的Agent API和捕获操作符来完成在地图上获取和放置值。
现在KV.Bucket
模块已经被定义,我们的测试应该通过!你可以通过运行:mix test
。
使用ExUnit回调测试设置
在继续并添加更多功能之前KV.Bucket
,让我们先谈谈ExUnit回调。正如您所预料的那样,所有KV.Bucket
测试都需要一个存储桶代理才能启动并运行。幸运的是,ExUnit支持回调,允许我们跳过这些重复的任务。
让我们重写测试用例以使用回调:
defmodule KV.BucketTest do
use ExUnit.Case, async: true
setup do
{:ok, bucket} = start_supervised(KV.Bucket)
%{bucket: bucket}
end
test "stores values by key", %{bucket: bucket} do
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
end
我们首先在setup/1
宏的帮助下定义了一个设置回调函数。该setup/1
回调在每个测试之前运行,与测试本身在相同的过程中运行。
请注意,我们需要一种机制将bucket
pid从回调传递到测试。我们通过使用测试环境
来这样做。当我们%{bucket: bucket}
从回调中返回时,ExUnit会将此映射合并到测试上下文中。由于测试环境
本身就是一张地图,我们可以对它进行模式匹配,从而在测试中提供对存储桶的访问:
test "stores values by key", %{bucket: bucket} do
# `bucket` is now the bucket from the setup block
end
您可以在ExUnit.Case
模块文档中阅读有关ExUnit案例的更多信息,以及关于ExUnit.Callbacks
文档中回调的更多信息。
其他代理行为
除了获取值和更新代理状态外,代理还允许我们通过一个函数调用来获取值并更新代理状态Agent.get_and_update/2
。让我们实现一个KV.Bucket.delete/2
从存储桶中删除一个密钥的函数,返回它的当前值:
@doc """
Deletes `key` from `bucket`.
Returns the current value of `key`, if `key` exists.
"""
def delete(bucket, key) do
Agent.get_and_update(bucket, &Map.pop(&1, key))
end
现在轮到你为上面的功能编写测试了!此外,请务必浏览该Agent
模块的文档以了解更多信息。
代理中的客户端/服务器
在继续下一章之前,我们来讨论代理中的客户端/服务器二分法。我们来扩展delete/2
我们刚刚实现的功能:
def delete(bucket, key) do
Agent.get_and_update(bucket, fn dict ->
Map.pop(dict, key)
end)
end
我们传递给代理的函数中的所有内容都发生在代理进程中。在这种情况下,由于代理进程是接收和响应我们消息的进程,我们说代理进程就是服务器。功能以外的所有内容都发生在客户端。
这个区别很重要。如果要执行昂贵的操作,则必须考虑在客户端上还是在服务器上执行这些操作会更好。例如:
def delete(bucket, key) do
Process.sleep(1000) # puts client to sleep
Agent.get_and_update(bucket, fn dict ->
Process.sleep(1000) # puts server to sleep
Map.pop(dict, key)
end)
end
当在服务器上执行一个长操作时,对该特定服务器的所有其他请求将等待操作完成,这可能会导致某些客户端超时。
在下一章中,我们将探讨GenServers,客户端和服务器之间的隔离更加明显。