在这篇文章中,我将向你介绍OTP,看看基本的进程循环、GenServer和Supervisor行为,并看看如何使用它们来实现一个存储资金的基本进程。
什么是OTP?
OTP是Elixir从Erlang继承的一套很棒的工具和库,Erlang是一种编程语言,它在其虚拟机上运行。
OTP包含很多东西,比如Erlang编译器、数据库、测试框架、剖析器、调试工具。但是,当我们在Elixir的上下文中谈论OTP时,我们通常指的是Erlang行为体模型,它基于轻量级的进程,是Elixir之所以如此高效的基础。
流程
.jpg)
在OTP的基础上,有一个叫做进程的小东西。
与操作系统的进程不同,它们是非常、非常轻量级的。创建它们只需要几秒钟,而一台机器可以轻松地同时运行数千个进程。
进程松散地遵循行为者模型。每个进程基本上都是一个可以接收信息的邮箱,并且可以对这些信息做出反应: \
- 创建新的进程。
- 向其他进程发送消息。
- 修改其私有状态。

催生进程
催生一个进程的最基本方法是使用spawn命令。让我们打开IEx并启动一个:
iex(1)> process = spawn(fn -> IO.puts("hey there!") end)
上面的函数将返回:
hey there!
#PID<0.104.0>
首先是函数的结果,其次是spawn的输出--PID,一个独特的进程识别号。
同时,我们的进程有一个问题。虽然它完成了我们要求它做的任务,但它现在好像......死了?😱
让我们使用它的PID(存储在变量process )来查询生命迹象。
iex(2)> Process.alive?(process)
false
如果你仔细想想,这是有道理的。这个进程做了我们要求它做的事情,完成了它存在的理由,并且关闭了自己。但是有一种方法可以延长进程的寿命,使它对我们更有价值。
接收-做的循环
事实证明,我们可以将进程功能扩展为一个可以保持状态并修改状态的循环。
例如,让我们想象一下,我们需要创建一个模仿皇宫国库中的资金的过程。我们将创建一个简单的流程,你可以向其存储或提取资金,并询问当前的余额。
我们将通过创建一个循环函数来做到这一点,该函数响应某些消息,同时保持其参数中的状态。
defmodule Palace.SimpleTreasury do
def loop(balance) do
receive do
{:store, amount} ->
loop(balance + amount)
{:withdraw, amount} ->
loop(balance - amount)
{:balance, pid} ->
send(pid, balance)
loop(balance)
end
end
end
在函数的主体中,我们把接收语句和模式匹配所有我们希望我们的进程响应的消息。每次循环运行时,它将从邮箱的底部(按收到的顺序)检查符合我们需要的消息,并处理它们。
如果进程看到任何带有原子store,withdraw,balance 的消息,这些将触发某些行动。
为了使它变得更漂亮,我们可以添加一个open 函数,也可以转储所有我们不需要的消息,以免污染邮箱。
defmodule Palace.SimpleTreasury do
def open() do
loop(0)
end
def loop(balance) do
receive do
{:store, amount} ->
loop(balance + amount)
{:withdraw, amount} ->
loop(balance - amount)
{:balance, pid} ->
send(pid, balance)
loop(balance)
_ ->
loop(balance)
end
end
end
end
虽然这看起来很简洁,但已经潜伏了一些模板,而且我们甚至还没有涵盖生产级代码所需的角落案例、跟踪和报告。
在现实生活中,我们不需要用接收做循环来写代码。相反,我们使用比我们聪明得多的人所创造的行为之一。
行为
许多流程都遵循某些类似的模式。为了对这些模式进行抽象,我们使用行为。行为有两个部分:我们不需要实现的抽象代码和特定于实现的回调模块。
在这篇文章中,我将向你介绍GenServer(通用服务器的简称)和Supervisor。这些并不是唯一的行为,但它们肯定是最常见的行为之一。
GenServer

首先,让我们创建一个名为Treasury 的模块,并在其中添加GenServer行为。
defmodule Palace.Treasury do
use GenServer
end
这将为该行为拉入必要的模板。之后,我们需要为我们的特定用例实现回调。
以下是我们将用于基本实现的内容。
| 回调 | 它做什么 | 它通常返回什么 |
init(state) | 初始化服务器。 | {:ok, state} |
handle_cast(pid, message) | 一个不要求服务器回答的异步调用。 | {:noreply, state} |
handle_call(pid, from, message) | 一个要求服务器回答的同步调用。 | {:reply, reply, state} |
让我们从最简单的开始 -init 。它需要一个状态并以该状态启动一个进程。
def init(balance) do
{:ok, balance}
end
现在,如果你看一下我们用receive 写的简单代码,有两种类型的触发器。第一种(store 和withdraw )只是要求国库异步更新其状态,而第二种(get_balance )则是等待回答。handle_cast 可以处理异步的,而handle_call 可以处理同步的。
为了处理加法和减法,我们将需要两个转换。这两个函数接收一个带有命令和交易额的消息,并更新状态。
def handle_cast({:store, amount}, balance) do
{:noreply, balance + amount}
end
def handle_cast({:withdraw, amount}, balance) do
{:noreply, balance - amount}
end
最后,handle_call 接受余额调用、调用者和状态,并使用所有这些来回复调用者并返回相同的状态。
def handle_call(:balance, _from, balance) do
{:reply, balance, balance}
end
这些就是我们所有的回调:
defmodule Palace.Treasury do
use GenServer
def init(balance) do
{:ok, balance}
end
def handle_cast({:store, amount}, balance) do
{:noreply, balance + amount}
end
def handle_cast({:withdraw, amount}, balance) do
{:noreply, balance - amount}
end
def handle_call(:balance, _from, balance) do
{:reply, balance, balance}
end
end
为了隐藏执行细节,我们可以在同一模块中添加客户端命令。由于这将是这个宫殿唯一的宝库,我们在用start_link 催生进程时,也给它一个等于其模块名称的名称。这将使它更容易被提及。
defmodule Palace.Treasury do
use GenServer
# Client
def open() do
GenServer.start_link(__MODULE__, 0, name: __MODULE__)
end
def store(amount) do
GenServer.cast(__MODULE__, {:store, amount})
end
def withdraw(amount) do
GenServer.cast(__MODULE__, {:withdraw, amount})
end
def get_balance() do
GenServer.call(__MODULE__, :balance)
end
# Callbacks
def init(balance) do
{:ok, balance}
end
def handle_cast({:store, amount}, balance) do
{:noreply, balance + amount}
end
def handle_cast({:withdraw, amount}, balance) do
{:noreply, balance - amount}
end
def handle_call(:balance, _from, balance) do
{:reply, balance, balance}
end
end
让我们试一试:
iex(1)> Palace.Treasury.open()
{:ok, #PID<0.138.0>}
iex(2)> Palace.Treasury.store(400)
:ok
iex(3)> Palace.Treasury.withdraw(100)
:ok
iex(4)> Palace.Treasury.get_balance()
300
它是有效的。🥳
监督员
.jpg)
然而,只是让库房运行而不进行监督是有点不负责任的,是失去资金或脑袋的好办法。😅
庆幸的是,OTP为我们提供了监督者的行为。监督员可以:
- 启动和关闭应用程序。
- 通过重启崩溃的进程来提供容错。
- 用来建立一个分层的监督结构,称为监督树。
让我们为我们的库房配备一个简单的监督器。
defmodule Palace.Treasury.Supervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def init(_init_arg) do
children = [
%{
id: Palace.Treasury,
start: {Palace.Treasury, :open, []}
}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
在最基本的情况下,一个监督者有两个功能。start_link(),将监督器作为一个进程运行,以及init ,提供监督器初始化所需的参数。
我们需要注意的事情是:
- 子进程的列表。 在这里,我们列出了所有我们希望监督者启动的进程,以及它们的初始函数和启动参数。每个进程都是一个映射,其中至少有
id和start键。 - 监督者的
init函数。 我们向它提供子进程的列表和一个监督策略。在这里,我们使用:one_for_one- 如果一个子进程将崩溃,只有该进程将被重新启动。还有一些。
运行Palace.Treasury.Supervisor.start_link() 函数将打开一个国库,这个国库将由该进程监督。如果国库崩溃了,它将以初始状态--0重新启动。
如果我们愿意,我们可以在这个监督者中加入其他几个与国库功能相关的进程,比如一个可以将掠夺的物品换成货币价值的进程。
此外,我们还可以复制或持久化国库进程的状态,以确保国库进程崩溃时我们的资金不会丢失。
由于这是一个基本指南,我将让你自己去研究各种可能性。