Elixir入门教程-进程

  1. 创建进程
  2. 发送和接收消息
  3. 链接
  4. 任务
  5. 状态

在Elixir,所有代码运行在进程里。进程彼此间隔离,彼此间并发运行,并且通过消息传递来交流。进程不仅是Elixir的并发基础,它也为构建分布式和容错程序提供手段。

Elixir的进程不应该和操作系统的进程混淆。Elixir的进程就内存和CPU的消耗来说是极其轻量的(不同于许多其他编程语言中的线程)。正因如此,成千上万的进程同时运行是很平常的。

在本章里,我们将学习关于创建新进程的基本结构以及进程间收发消息。

创建进程

创建新进程的基本机制是自动导入的 spawn/1 函数:

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

spawn/1 的入参是一个函数,这个函数将在另一个进程里执行。

注意 spawn/1 返回一个PID(进程标识符)。像如上的例子,你产生的过程很可能是死的。被创建的进程将执行给定的函数,并且在函数结束后退出。

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

注意:你将很可能获得一个不同于我们这个例子里获得的进程标识符。

我们可以调用 self/0 来获取当前进程的PID:

1
2
3
4
iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

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

发送和接收消息

我们可以用 send/2 发送消息给一个进程并且可以用 receive/1 接收消息。

1
2
3
4
5
6
7
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 上,它只是将消息放入接收者的邮箱然后继续执行后面的语句。特别是,进程可以给自己发送消息。在上面的例子里,当 receive 语句块获得执行的时候,发送者进程可能已经死掉了。

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

1
2
3
4
5
6
iex> receive do
...> {:hello, msg} -> msg
...> after
...> 1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

当你已经期待的消息已经在邮箱里的时候,可以将超时设置为0。

让我们把这些放在一起并在进程间发送消息:

1
2
3
4
5
6
7
8
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>"

当在使用shell的时候,你可能发现帮助函数 flush/0 非常有用。它刷新并打印邮箱里的所有消息。

1
2
3
4
5
iex> send self(), :hello
:hello
iex> flush()
:hello
:ok

链接

在Elixir里最通用的创建进程的方式实际上是用 spawn_link/1 函数。在我们展示 spawn_link/1 的例子之前,让我们尝试看看当一个进程失败的时候会发生什么:

1
2
3
4
5
6
iex> spawn fn -> raise "oops" end
#PID<0.58.0>
[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
:erlang.apply/2

它只是记录一个错误,而创建者依然运行。这是因为进程间是隔离的。如果我们想一个进程的失败会传递给另一个进程,我们应该将它们链接在一起。这可以用 spawn_link/1 来做到:

1
2
3
4
5
6
iex> spawn_link fn -> raise "oops" end
#PID<0.41.0>
** (EXIT from #PID<0.41.0>) an exception was raised:
** (RuntimeError) oops
:erlang.apply/2

当在shell里发生一个失败,shell自动地捕获这个失败并以合适的格式展现出来。为了理解在我们的代码里到底将发生什么,让我们在一个文件里使用 spawn_link/1 ,并运行它:

1
2
3
4
5
6
# spawn.exs
spawn_link fn -> raise "oops" end
receive do
:hello -> "let's wait until the process fails"
end
1
2
3
4
5
$ elixir spawn.exs
** (EXIT from #PID<0.47.0>) an exception was raised:
** (RuntimeError) oops
spawn.exs:1: anonymous fn/0 in :elixir_compiler_0.__FILE__/1

这一次,进程失败并且把它的父进程也搞垮了,因为它们是链接的。链接也可以通过调用 Process.link/1 来手工做到。我们建议你看一下 Process 模块来研究进程提供的其他功能。

当构建容错系统的时候,进程和链接扮演重要角色。在Elixir的应用里,我们经常链接我们的进程到一个监督者,当一个进程死掉的时候,这个监督者可以检测到,并且在那个地方启动一个新进程。这是唯一可能的,因为进程是隔离的,默认情况下不共享任何东西。并且因为进程是隔离的,因此没有方法在一个进程失败的时候崩溃或破坏另一个进程的状态。

其他语言需要我们捕获和处理异常,而在Elixir里,我们实际上是乐于让进程失败,因为我们期望监督者正确地重新启动我们的系统。当我们写Elixir程序的时候,“快速失败”是一种常见的哲学!

在Elixir里,spawn/1 和 spawn_link/1 是创建进程的基本命令。虽然目前为止我们只使用了它们两个,但是绝大多数时候我们将使用构建于它们之上的抽象。让我们看看这些抽象中最常用的一个,它叫做任务。

任务

任务构建在创建进程函数之上,以提供更好的错误报告和内省:

1
2
3
4
5
6
7
8
9
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
(elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2
(stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3
Function: #Function<20.90072148/0 in :erl_eval.expr/5>
Args: []

我们用 Task.start/1 和 Task.start_link/1 替代 spawn/1 和 spawn_link/1 ,它们返回 {:ok, pid} ,而不只是PID。这就是为什么使得任务被用在监督树里。而且Task提供像 Task.async/1 和 Task.await/1 这样的便捷的函数以及易于分布式的功能。

我们将在 Mix 和 OTP 指导 里探索这些功能,现在记住使用 Task 来获得更好的错误报告就足够了。

状态

到目前为止,我们的教程还没有讨论过状态。如果你正在构建一个需要状态的应用,例如,保存你的应用配置,或者你需要分析一个文件并且保存在内存里,那么你需要保存它在哪里?

对于这个问题,进程是最通用的回答。我们可以写无限循环的,维护状态的,并且收发消息的进程。作为一个例子,让我们写一个模块,它开始一个新进程,这个进程像一个键值对存储一样工作,这个模块在名字为kv.exs的文件里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ,这个新版本的映射存储了给定的键和值。

让我们通过运行 iex kv.exs 来试试:

1
2
3
4
5
6
7
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 消息来试试:

1
2
3
4
5
6
7
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发送消息并且操作它的进程。

注册这个pid并给它一个名字也是可以的,并且允许所有知道它名字的进程都可以给它发送消息。

1
2
3
4
5
6
7
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提供了 代理者 ,它是以状态为基础的简单抽象:

1
2
3
4
5
6
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提供了一个API来构建通用服务(叫做 GenServer),任务,以及其他更多事务,这些全部由下面的进程驱动。这些,连同监督树,将在 Mix 和 OTP 指导(这个指导将从开始到结束构建一个完整的Elixir应用) 里更详细地探讨。

接下来,让我们探索Elixir的I/O世界。

原文链接: http://elixir-lang.org/getting-started/processes.html