处理

在 Elixir 中,所有代码都在进程内运行。进程彼此隔离,彼此同时运行并通过消息传递进行通信。流程不仅是 Elixir 并发的基础,而且还提供了构建分布式和容错程序的手段。

Elixir 的流程不应与操作系统流程混淆。Elixir 中的进程在内存和 CPU 方面非常轻量级(与其他许多编程语言中的线程不同)。因此,数十甚至数十万个进程同时运行并不罕见。

在本章中,我们将了解产生新进程的基本结构,以及在进程之间发送和接收消息。

spawn

产生新进程的基本机制是自动导入spawn/1功能:

iex> spawn fn -> 1 + 2 end
#PID<0.43.0>

spawn/1 接受一个将在另一个进程中执行的函数。

通知spawn/1返回一个 PID(进程标识符)。在这一点上,你产生的过程很可能已经死亡。生成的进程将执行给定的函数,并在函数完成后退出:

iex> pid = spawn fn -> 1 + 2 end
#PID<0.44.0>
iex> Process.alive?(pid)
false

注意:您可能会获得与本指南中获得的流程标识符不同的流程标识符。

我们可以通过调用self/0以下命令来检索当前进程的 PID :

iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

当我们能够发送和接收消息时,进程变得更加有趣。

sendreceive

我们可以将消息发送到与流程send/2,并与接收它们receive/1

iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...>   {:hello, msg} -> msg
...>   {:world, msg} -> "won't match"
...> end
"world"

当消息发送到进程时,消息存储在进程邮箱中。receive/1块通过当前进程邮箱搜索与任何给定模式匹配的消息。receive/1支持警卫和许多条款,比如case/2

发送邮件的过程不会阻止send/2,它会将邮件放入收件人的邮箱并继续。特别是,一个进程可以发送消息给自己。

如果邮箱中没有匹配任何模式的消息,则当前进程将一直等到匹配的消息到达。超时也可以被指定:

iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

如果您已经预期邮件在邮箱中,则可以给出超时值0。

让我们把它放在一起,并在进程之间发送消息:

iex> parent = self()
#PID<0.41.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.48.0>
iex> receive do
...>   {:hello, pid} -> "Got hello from #{inspect pid}"
...> end
"Got hello from #PID<0.48.0>"

inspect/1函数用于将数据结构的内部表示转换为字符串,通常用于打印。请注意,当receive块被执行时,我们产生的发送者进程可能已经死了,因为它的唯一指令是发送消息。

在 shell 中,您可能会发现该帮助程序flush/0非常有用。它会刷新并打印邮箱中的所有邮件。

iex> send self(), :hello
:hello
iex> flush()
:hello
:ok

链接

我们大多数时候在Elixir中产生进程,我们将它们产生为链接进程。在我们展示一个例子之前spawn_link/1,让我们看看当一个进程启动spawn/1失败时会发生什么:

iex> spawn fn -> raise "oops" end
#PID<0.58.0>

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

它只记录了一个错误,但父进程仍在运行。这是因为流程是孤立的。如果我们希望一个过程中的失败传播给另一个过程,我们应该将它们联系起来。这可以通过以下方式完成spawn_link/1

iex> self()
#PID<0.41.0>
iex> spawn_link fn -> raise "oops" end

** (EXIT from #PID<0.41.0>) evaluator process exited with reason: an exception was raised:
    ** (RuntimeError) oops
        (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

[error] Process #PID<0.289.0> raised an exception
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6

由于进程是链接的,我们现在看到一条消息,说明父进程(即进程)从另一个进程收到 EXIT 信号,导致 shell 终止。IEx 检测到这种情况并开始一个新的 shell 会话。

链接也可以通过调用手动完成Process.link/1。我们建议您先看看Process模块由过程提供的其他功能。

在构建容错系统时,进程和链接扮演着重要角色。Elixir 进程是孤立的,默认情况下不会共享任何内容。因此,一个进程中的失败永远不会崩溃或破坏另一个进程的状态。但是,链接允许流程在出现故障时建立关系。我们经常将我们的流程与监督人员联系起来,这些监督人员会检测流程何时死亡并开始新的流程。

虽然其他语言会要求我们捕捉/处理异常,但在 Elixir 中,我们确实可以让流程失败,因为我们期望主管能够正确地重新启动我们的系统。在编写Elixir软件时,“快速失败”是一种常见的理念!

spawn/1并且spawn_link/1是用于在 Elixir 中创建进程的基本原型。尽管到目前为止我们已经完全使用它们,但大部分时间我们都会使用基于它们的抽象。让我们看看最常见的一个,称为任务。

任务

任务建立在 spawn 函数之上,以提供更好的错误报告和反思:

iex(1)> Task.start fn -> raise "oops" end
{:ok, #PID<0.55.0>}

15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating
** (RuntimeError) oops
    (stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
    (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: #Function<20.99386804/0 in :erl_eval.expr/5>
    Args: []

而不是spawn/1spawn_link/1我们使用Task.start/1Task.start_link/1其返回{:ok,pid},而不仅仅是PID。这是使任务能够用于监督树的原因。此外,Task提供了方便的功能,如Task.async/1Task.await/1,和功能性来缓解分布。

我们将在 Mix 和 OTP 指南中探索这些功能,现在只需记住使用它就Task可以获得更好的错误报告。

状态

在本指南中,我们尚未讨论过目前的状态。如果你正在构建一个需要状态的应用程序,例如,为了保持你的应用程序配置,或者你需要解析一个文件并将它保存在内存中,你将在哪里存储?

过程是这个问题最常见的答案。我们可以编写无限循环的进程,维护状态以及发送和接收消息。作为一个例子,我们来编写一个模块,它启动一个新的进程,在一个名为kv.exs

defmodule KV do
  def start_link do
    Task.start_link(fn -> loop(%{}) end)
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end

请注意,start_link函数启动一个运行loop/1函数的新进程,从一个空映射开始。loop/1函数然后等待消息并为每条消息执行适当的操作。在:get消息的情况下,它将消息发回给呼叫者并loop/1再次呼叫,以等待新消息。虽然:put消息实际上是loop/1用给定的keyvalue存储的地图的新版本调用的。

让我们试一试iex kv.exs*

iex> {:ok, pid} = KV.start_link
{:ok, #PID<0.62.0>}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush()
nil
:ok

起初,流程图没有键,所以发送一条:get消息,然后刷新当前进程的收件箱返回nil。让我们发送一条:put消息并再次尝试:

iex> send pid, {:put, :hello, :world}
{:put, :hello, :world}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok

注意过程如何保持一个状态,我们可以通过发送过程消息来获取和更新这个状态。事实上,任何了解pid上述过程的人都可以发送消息并操纵状态。

也可以注册pid,给它一个名称,并允许每个知道该名称的人发送消息:

iex> Process.register(pid, :kv)
true
iex> send :kv, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok

使用进程来维护状态和名称注册是 Elixir 应用程序中非常常见的模式。但是,大多数情况下,我们不会像上面那样手动实现这些模式,而是使用 Elixir 附带的许多抽象中的一种。例如,Elixir提供代理,这些代理是对状态的简单抽象:

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

:name还可以给出一个选项Agent.start_link/2,它会自动注册。除代理之外,Elixir 还提供了一个用于构建通用服务器(调用GenServer),任务等的 API ,所有服务均由下面的进程提供支持。这些以及监督树将在 Mix 和 OTP 指南中进行更详细的探讨,该指南将从头到尾构建完整的Elixir应用程序。

现在,让我们继续探索 Elixir 的 I/O 世界吧。