处理
在 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
当我们能够发送和接收消息时,进程变得更加有趣。
send
和 receive
我们可以将消息发送到与流程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/1
和spawn_link/1
我们使用Task.start/1
和Task.start_link/1
其返回{:ok,pid}
,而不仅仅是PID。这是使任务能够用于监督树的原因。此外,Task
提供了方便的功能,如Task.async/1
和Task.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
用给定的key
和value
存储的地图的新版本调用的。
让我们试一试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 世界吧。