在Elixir,所有代码运行在进程里。进程彼此间隔离,彼此间并发运行,并且通过消息传递来交流。进程不仅是Elixir的并发基础,它也为构建分布式和容错程序提供手段。
Elixir的进程不应该和操作系统的进程混淆。Elixir的进程就内存和CPU的消耗来说是极其轻量的(不同于许多其他编程语言中的线程)。正因如此,成千上万的进程同时运行是很平常的。
在本章里,我们将学习关于创建新进程的基本结构以及进程间收发消息。
创建进程
创建新进程的基本机制是自动导入的 spawn/1 函数:
|
|
spawn/1 的入参是一个函数,这个函数将在另一个进程里执行。
注意 spawn/1 返回一个PID(进程标识符)。像如上的例子,你产生的过程很可能是死的。被创建的进程将执行给定的函数,并且在函数结束后退出。
|
|
注意:你将很可能获得一个不同于我们这个例子里获得的进程标识符。
我们可以调用 self/0 来获取当前进程的PID:
|
|
当我们能够发送和接收消息时,进程变得更加有趣。
发送和接收消息
我们可以用 send/2 发送消息给一个进程并且可以用 receive/1 接收消息。
|
|
当一条消息发送给一个进程,这个消息存储在进程的邮箱里。receive/1 语句块遍历当前邮箱来查找任何匹配给定模式的消息。receive/1 支持卫语句和许多分支,就如 case/2 一样。
发送消息的进程不会阻塞在 send/2 上,它只是将消息放入接收者的邮箱然后继续执行后面的语句。特别是,进程可以给自己发送消息。在上面的例子里,当 receive 语句块获得执行的时候,发送者进程可能已经死掉了。
如果没有邮箱里的消息匹配任何模式,则当前进程将等待一直到一个匹配的消息到来。超时也可以被指定:
|
|
当你已经期待的消息已经在邮箱里的时候,可以将超时设置为0。
让我们把这些放在一起并在进程间发送消息:
|
|
当在使用shell的时候,你可能发现帮助函数 flush/0 非常有用。它刷新并打印邮箱里的所有消息。
|
|
链接
在Elixir里最通用的创建进程的方式实际上是用 spawn_link/1 函数。在我们展示 spawn_link/1 的例子之前,让我们尝试看看当一个进程失败的时候会发生什么:
|
|
它只是记录一个错误,而创建者依然运行。这是因为进程间是隔离的。如果我们想一个进程的失败会传递给另一个进程,我们应该将它们链接在一起。这可以用 spawn_link/1 来做到:
|
|
当在shell里发生一个失败,shell自动地捕获这个失败并以合适的格式展现出来。为了理解在我们的代码里到底将发生什么,让我们在一个文件里使用 spawn_link/1 ,并运行它:
|
|
|
|
这一次,进程失败并且把它的父进程也搞垮了,因为它们是链接的。链接也可以通过调用 Process.link/1 来手工做到。我们建议你看一下 Process 模块来研究进程提供的其他功能。
当构建容错系统的时候,进程和链接扮演重要角色。在Elixir的应用里,我们经常链接我们的进程到一个监督者,当一个进程死掉的时候,这个监督者可以检测到,并且在那个地方启动一个新进程。这是唯一可能的,因为进程是隔离的,默认情况下不共享任何东西。并且因为进程是隔离的,因此没有方法在一个进程失败的时候崩溃或破坏另一个进程的状态。
其他语言需要我们捕获和处理异常,而在Elixir里,我们实际上是乐于让进程失败,因为我们期望监督者正确地重新启动我们的系统。当我们写Elixir程序的时候,“快速失败”是一种常见的哲学!
在Elixir里,spawn/1 和 spawn_link/1 是创建进程的基本命令。虽然目前为止我们只使用了它们两个,但是绝大多数时候我们将使用构建于它们之上的抽象。让我们看看这些抽象中最常用的一个,它叫做任务。
任务
任务构建在创建进程函数之上,以提供更好的错误报告和内省:
|
|
我们用 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的文件里:
|
|
注意:start_link 函数启动一个新进程来运行 loop/1 函数,这个函数以一个空的映射为入参。loop/1 函数然后等待消息,并且为每个消息执行适当的操作。如果是一个 :get 消息,它发送一个消息回去给调用者并再次调用 loop/1 ,接着等待新的消息。而 :put 消息的话,实际上用一个新版本的映射作为入参调用 loop/1 ,这个新版本的映射存储了给定的键和值。
让我们通过运行 iex kv.exs 来试试:
|
|
首先,进程的映射没有键,那么发送一个 :get 消息给它然后刷新当前进程的邮箱将返回 nil ,让我发送一个 :put 消息来试试:
|
|
注意进程如何保持状态,并且我们通过给进程发送消息来获取和修改这个状态。实际上,任何进程只要知道上面例子的pid都能够给这个pid发送消息并且操作它的进程。
注册这个pid并给它一个名字也是可以的,并且允许所有知道它名字的进程都可以给它发送消息。
|
|
在Elixir应用里用进程来维护状态和名字注册是非常通用的模式。但是,绝大多数时候,我们不需要像上面例子一样来手工实现那些模式,而是使用Elixir自带的许多抽象之一就可以。例如,Elixir提供了 代理者 ,它是以状态为基础的简单抽象:
|
|
一个 :name 选项一个可以传给 Agent.start_link/2 ,这样它将被自动注册。除了代理者,Elixir提供了一个API来构建通用服务(叫做 GenServer),任务,以及其他更多事务,这些全部由下面的进程驱动。这些,连同监督树,将在 Mix 和 OTP 指导(这个指导将从开始到结束构建一个完整的Elixir应用) 里更详细地探讨。
接下来,让我们探索Elixir的I/O世界。