Elixir中的OTP简要指南

461 阅读7分钟

在这篇文章中,我将向你介绍OTP,看看基本的进程循环、GenServer和Supervisor行为,并看看如何使用它们来实现一个存储资金的基本进程。

什么是OTP?

OTP是Elixir从Erlang继承的一套很棒的工具和库,Erlang是一种编程语言,它在其虚拟机上运行。

OTP包含很多东西,比如Erlang编译器、数据库、测试框架、剖析器、调试工具。但是,当我们在Elixir的上下文中谈论OTP时,我们通常指的是Erlang行为体模型,它基于轻量级的进程,是Elixir之所以如此高效的基础。

流程

Processes divider

在OTP的基础上,有一个叫做进程的小东西。

与操作系统的进程不同,它们是非常、非常轻量级的。创建它们只需要几秒钟,而一台机器可以轻松地同时运行数千个进程。

进程松散地遵循行为者模型。每个进程基本上都是一个可以接收信息的邮箱,并且可以对这些信息做出反应: \

  • 创建新的进程。
  • 向其他进程发送消息。
  • 修改其私有状态。

Process graph

催生进程

催生一个进程的最基本方法是使用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

GenServer divider

首先,让我们创建一个名为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 写的简单代码,有两种类型的触发器。第一种(storewithdraw )只是要求国库异步更新其状态,而第二种(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

它是有效的。🥳

监督员

Supervisor divider

然而,只是让库房运行而不进行监督是有点不负责任的,是失去资金或脑袋的好办法。😅

庆幸的是,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 ,提供监督器初始化所需的参数。

我们需要注意的事情是:

  • 子进程的列表。 在这里,我们列出了所有我们希望监督者启动的进程,以及它们的初始函数和启动参数。每个进程都是一个映射,其中至少有idstart 键。
  • 监督者的init 函数。 我们向它提供子进程的列表和一个监督策略。在这里,我们使用:one_for_one - 如果一个子进程将崩溃,只有该进程将被重新启动。还有一些。

运行Palace.Treasury.Supervisor.start_link() 函数将打开一个国库,这个国库将由该进程监督。如果国库崩溃了,它将以初始状态--0重新启动。

如果我们愿意,我们可以在这个监督者中加入其他几个与国库功能相关的进程,比如一个可以将掠夺的物品换成货币价值的进程。

此外,我们还可以复制或持久化国库进程的状态,以确保国库进程崩溃时我们的资金不会丢失。

由于这是一个基本指南,我将让你自己去研究各种可能性。