Gen 服务器——抽象状态管理和任务一起运行
在 Elixir 中,每个函数都在一个单独的进程中运行,我们可以通过语言提供的非常自然的方式处理进程。我们可以通过一些已知的抽象(例如)来管理多个长时间运行的进程之间的状态Agent
。在本文中,我们将简要介绍一种抽象,它允许管理长时间运行的进程,并与正在运行的函数进行状态和消息通信!我希望它有趣且易于理解。
目录
什么是 Gen 服务器以及它解决了什么问题
在考虑应该尝试哪种实现方式之前,理解我们要解决的问题非常重要,对吧?所以,这里的问题在于对长时间运行的进程缺乏抽象。目前,我们可以编写自己的模块来处理某些状态,并实现一个使用receive
块创建“邮箱”的方法,或者我们可以使用诸如Agent
和Task
之类的简单抽象来仅保存状态或仅管理异步任务。
首先,让我们回顾一下,了解如何使用更简单的抽象在流程中共享状态。如果您想更详细地了解这些模块,请参阅开头链接的上一篇文章。:)
代理人
该Agent
模块提供了一种简单高效的状态管理方法。我个人将其视为进程架构的数据结构,因为您可以通过 pid 和 getter/setter 控制任何原始值。它非常易于使用,如以下示例所示:
# Creating an agent
{:ok, agent} = Agent.start_link(fn -> [] end)
# Updating the state inside an agent
Agent.update(agent, fn list -> ["cherry" | list] end)
# Retrieving the state inside an agent
Agent.get(agent, fn list -> list end)
Agent.get(agent, fn list -> Enum.find(fn item -> item == "cherry" end) end)
如您所见,我们有一个 getter 和一个 setter 方法,它们都接受一个函数回调来管理内部状态,通过这个简单的 API,我们可以仅管理数据结构。
任务
我们可以与进程一起使用的另一种抽象是Task
围绕处理异步进程(特别是抽象函数)的思想工作的模块spawn
。它有一个非常简单的 API,其工作原理如下例所示:
# To initiate a new task
task = Task.async(fn ->
# Do whatever you want here
IO.puts("Task is running")
42
end)
# Get the return value of this particular task
answer_to_everything = Task.await(task)
如果您了解 JavaScript 或 csharp 等语言,您可能会记得async
await
它们的语法。
说了这么多,你可能会想,既然已经有了上面提到的模块,为什么还要使用 Gen Server?其实,使用 Gen Server 就相当于用一个简单易用的 API 将所有这些概念(异步进程和状态管理)粘合在一起,并且实现了一个非常重要的概念:mailbox
基于消息的架构。
邮箱是 Elixir 社区中的一个常用术语,指的是一个可以接收消息并对其执行操作的模块。常用的方法是使用
receive
代码块(无论是内部还是显式的)通过模式匹配来处理这些消息,并生成另一个进程来执行相应的操作。
使用这种抽象,我们可以通过handle
为每条消息定义方法并对其进行正确的操作来分离客户端和服务器代码。我们将在下面看到更多!
如何使用 Gen 服务器
Gen Server 抽象提供了一种架构,允许我们将模块代码分离为消息发送方和处理方。可以使用GenServer.call
或GenServer.cast
消息原子通过模式匹配来激活特定方法。
例如,在使用Agent
模块时,您很可能会将所有逻辑保存在同一个位置,如下所示:
def put(bucket, key, value) do
# Here is the client code
Agent.update(bucket, fn state ->
# Here is the server code
Map.put(state, key, value)
end)
# Back to the client code
end
由于Agent
模块没有提供足够的抽象,你只能在函数中提供需要对状态采取行动的函数update
。但是通过使用基于消息的抽象,我们可以进一步模块化代码,如下所示:
# Client code
def put(pid, key, value) do
GenServer.call(pid, {:insert, key, value})
end
# Server code
def handle_call({:insert, key, value}, _from, state) do
{:reply, :ok, Map.put(state, key, value)}
end
看到了吗?我们只需利用 Elixir 模式匹配的强大功能以及 Gen 服务器提供的消息架构,就能分离出更多代码。
这种抽象的使用非常简单;您需要按如下方式实现行为:
defmodule Example do
use GenServer
# Client world (we'll add later)
# Server world
@impl true
def init(state) do
{:ok, state}
end
@impl true
def handle_call(:list_all, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast({:insert, name}, state) do
:timer.sleep(5000)
{:noreply, [name | state]}
end
end
init
当我们使用 启动 Gen Server 时,该函数将被调用GenServer.start_link
。这作为我们定义将要管理的初始状态的入口点。- 该
handle_call
函数进入了“邮箱”接收者的领域,当我们使用GenServer.call
一个特定的、模式匹配的操作(:list_all
在示例中)时,该函数将被调用。还需要指出的是,这些函数将同步运行。 - 该
handle_cast
函数的用途与 相同handle_call
;它将通过调用被调用GenServer.cast
,并且其模式匹配正确。重要的区别在于,该函数将异步运行。
了解服务器部分之后,让我们用一些客户端代码 API 来补充它,以便在 REPL 上调用:
defmodule Example do
use GenServer
# Client world
def add_name(pid, name) do
GenServer.cast(pid, {:insert, name})
end
def list_names(pid) do
GenServer.call(pid, :list_all)
end
# Server world
# ...
end
在这种特定情况下,客户端代码将作为外部世界的便捷 API,因此我们无需GenServer
始终引用该模块。借助这组方法,我们可以轻松处理同步代码(带有 的 list_names 方法call
)和异步代码(带有 的 add_name 方法cast
)!
还请注意,我们handle_cast
故意:insert
使用计时器休眠 5 秒!下面我们将展示它在 REPL 上的工作原理,以了解异步和同步调用之间的区别。
要查看正确的延迟,请观看以下视频:
要引用 REPL 上使用的方法:
iex(1)> {:ok, pid} = GenServer.start_link(Example, [])
{:ok, #PID<0.135.0>}
iex(2)> Example.list_names(pid)
[]
iex(3)> Example.add_name(pid, "Cherry")
:ok
iex(4)> Example.list_names(pid)
["Cherry"]
该add_name
方法立即返回,但该list_names
方法受到我们强制设置的计时器的影响。这两个概念之间的平衡使得这个抽象真正实用且灵活!
参考
- https://elixir-lang.org/getting-started/mix-otp/genserver.html
- https://dev.to/reichert621/learning-elixir-s-genserver-with-a-real-world-example-5fef
- https://hexdocs.pm/elixir/1.12/GenServer.html
结论
这是在 Elixir 中学习流程的又一部分!希望对任何读到这篇文章的人有所帮助,这对我来说是一次很棒的经历,因为我一边写作,一边学习这门语言以及所有这些新概念。如果我能帮上什么忙,尽管联系我!愿原力与你同在🍒
文章来源:https://dev.to/cherryramatis/gen-servers-abstracting-state-management-and-task-run-together-hpd