11 进程

Elixir的所有代码都运行在进程。每个进程之间互相隔离,同时运行,通过传递消息进行通讯。进程不仅是Elixir并发的基础,而且它们也是编写分布式和高可靠性程序的基础。

和其他语言中的线程不同,就内存和CPU消耗来说,进程是极为轻量级的。同时运行上千个进程是家常便饭。

在这一章,我们将学习如何创建一个新的进程,包括在进程之间首发信息。

11.1 spawn

创建一个进程最基本的方式是使用内置的spawn/1函数:

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

spawn/1接受一个函数,然后在另一个进程中执行。

注意spawn/1返回一个PID(进程识别符)。在这个时候看起来你的进程很可能已经死掉了。被创建的进程在执行完传进来的函数之后会自动退出:

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

注意:自己动手的话,你应该会得到一个和我们上面例子不同的PID

我们能通过调用self/0得到当前进程的PID:

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

只有当我们能够在进程之间首发信息是,它才会变得更加有趣。

11.2 发和送

我们能用send/2向一个进程发送信息,能用receive/1接受信息。

  1. iex> send self(), {:hello, "world"}
  2. {:hello, "world"}
  3. iex> receive do
  4. ...> {:hello, msg} -> msg
  5. ...> {:world, msg} -> "won't match"
  6. ...> end
  7. "world"

当一个消息被送到一个进程的时候,它首先被储存在进程的邮箱里。receive/2会在邮箱中寻找第一个匹配模式的消息,receive/1支持许多子句,比如case/2,包括子句中的守护。

如果邮箱中没有消息匹配指定的模式,当前的进程会一直等待直到来了一个匹配的消息。当然你可以指定等待的最长时间:

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

如果早就确信消息就在邮箱里,那可以把timeout设为0。

让我们汇总一下看看如何在进程间发送消息:

  1. iex> parent = self()
  2. #PID<0.41.0>
  3. iex> spawn fn -> send(parent, {:hello, self()}) end
  4. #PID<0.48.0>
  5. iex> receive do
  6. ...> {:hello, pid} -> "Got hello from #{inspect pid}"
  7. ...> end
  8. "Got hello from #PID<0.48.0>"

如果你在控制台里,那函数flush/0可能就对你比较有用。它能打印并清空整个邮箱。

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

在我们完成这章之前,让我们谈一谈进程之间的链接。

11.3 链接

实际上,在Elixir中最长用到的创建进程的方式是调用函数spawn_link/1。在展示spawn_link/1的例子之前,让我们试着看看当进程出错的时候会发生什么:

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

正如你所见。。。什么也没有发生。那是因为进程都是独立的。如果我们希望一个进程的问题能够传导至另一个进程,我们就应该把它们链接起来。这个时候就该spawn_link/1出场了:

  1. iex> spawn_link fn -> raise "oops" end
  2. #PID<0.60.0>
  3. ** (EXIT from #PID<0.60.0>) {RuntimeError[message: "oops"], [{:erlang, :apply, 2, []}]}

当控制台中除了错,控制台自动处理它们,并且打印出整齐的错误信息。为了能真正理解刚才在我们的代码中发生了什么,让我们使用在一个脚本中使用spawn_link/1,并运行看看:

  1. # spawn.exs
  2. spawn_link fn -> raise "oops" end
  3. receive do
  4. :hello -> "let's wait until the process fails"
  5. end

这一次,当子进程退出,也使得母进程退出了,因为它们是链接在一起的。链接可以通过调用Process.link/2手动完成。我们见你你看看Process模块中还有那些其他的函数。

在编写高可靠性的系统时, 进程和链接扮演了重要的角色。在Elixir的应用中,我们常常把我们的进程同监工链接在一起,它的任务是当发现有进程退出的时候,在原地重启一个新的进程。之所以能这么干,就是得益于进程之间互相独立,某一个进程的问题不会导致其他进程出问题。

在其他的一些语言中,它也许要求你捕捉和处理意外,在Elixir的中我们可以随它去,因为我们直到监工会去重启进程。“早出问题”是编写Elixir软件时的常用哲学思想。

下面我们将探索IO的世界。