使用 Actor 就像租车——如果我们需要,可以很快速的租到一辆;如果车辆发生故障,也不需要自己修理,直接换一辆即可。
Actor 模型是一种适用性非常好的通用并发编程模型。它可以应用于共享内存架构和分布式内存架构,适合解决地理分布问题,同时还能提供很好的容错性。
函数式编程使用可变状态,也就避免了共享可变状态带来的一系列问题。相比之下,使用 Actor 模型保留了可变状态,但不将其共享。
Actor 类似于 OOP 中的对象——其中封装了状态,并通过消息与其他 Actor 通信。两者的区别是所有 Actor 可以同时运行,而且,与 OO 式的“消息传递(实质上是方法调用)”不同,actor 之间是真实的在传递消息。
Actor 模型是一个通用的并发编程模型,几乎可以用在任何一种编程语言里,最典型的是 Erlang。而我们将使用 Elixir 来介绍 actor 模型,它是 Erlang 虚拟机(BEAM)上一种较新的编程语言。
与 Clojure 相比,Elixir 是一种不纯粹的、动态类型的函数式语言。
在 Elixir 中,进程是一个轻量级的概念,比操作系统的线程还要轻,它消耗更少的资源且创建代价很低。Elixir 程序可以毫不困难的创建数千个进程,通常不需要依赖线程池技术。
异步的发送消息是使用 actor 模型的重要特性之一。消息并非直接发送到一个 actor,而是发送到一个 mailbox。
这样的设计解耦了 actor 之间的关系——actor 都以自己的步调运行,发送消息时也不会被阻塞。
虽然所有 Actor 可以同时运行,但它们都按照信箱接收到消息的顺序来依次处理消息,且仅在当前消息处理完成之后才会开始处理下一条消息,因此我们只需要关心发送消息时的并发问题即可。
1 | def loop do |
通常 actor 会进行无限循环,通过 receive 等待接收消息,并进行消息处理。在 Elixir 的 actor 实现中,内部的一个函数通过递归调用自己来进行无限循环,用 receive 来等待一个消息,通过模式匹配来决定如何处理消息。这
Elixir 实现了尾调用消除,即,如果函数在最后调用了自己,那么递归调用将被替换成一个简单的跳转,这样可以避免递归引起的堆栈移除。
为了彻底关闭一个 actor,需要满足两个条件。第一个是需要告诉 actor 在完成消息处理后就关闭;第二个是需要知道 actor 何时完成关闭。
首先,通过接收一个显式的关闭消息来满足第一个条件:
1 | receive do |
然后,通过一个方法来获知 actor 是否完全关闭。下面的代码将 :trap_exit
设为 true,并用 spawn_link 替换 spawn 以连接到进程:
1 | Process.flag(:trap_exit, true) |
现在当创建的进程关闭时,就会得到一个通知(是一个系统产生的消息)。
Actor 是以异步的方式发送消息的——发送者因此不会被阻塞。那么如何获得一个消息的回复呢?
Actor 模型没有提供直接回复消息的机制,但我们可以轻松实现:将发送进程的标示符包含在消息中,接收者接收到消息后提取其中的标识符,然后向该标识符表示的进程发送回复消息。
将一个消息发送给某个进程时,需要知道进程的标示符。当我们自己创建进程时没有问题,但如何向别人创建的进程发送消息呢?最简单的方式就是为进程命名。
前面我们使用 spawn_link 建立了两个进程之间的连接,这样就可以检测到某个进程的终止。Linking 是 Elixir 编程中的一个重要概念。
可以创建一个系统进程来管理其他若干个进程。
Tony Hoare 有一句名言: 软件设计有两种方式:一种是使软件过于简单,明显的没有缺陷;另一种是使软件过于复杂,没有明显的缺陷。
Actor 提供了一种容错的方式:错误处理内核模式。在两者之间找到了一种平衡。
一个软件系统如何应用了错误处理内核模式,那么使该系统正确运行的前提是其错误处理内核必须能够正确运行。程序的程序通常使用尽可能小的错误处理内核——小而简单到明显没有缺陷。
对于一个使用 actor 模型的程序,其错误处理内核是顶层的管理者,管理着子进程——对子进程进行启动、停止、重启等操作。
程序的每个模块都有自己的错误处理内核——模块正确运行的前提是其错误处理内核必须正确运行。子模块也会拥有自己的错误处理内核,依次类推。这就构成了一个错误处理的层级树,较危险的操作都会被下放给底层的 actor 执行。
错误处理内核机制主要解决了防御式编程中碰到的一些棘手问题。
防御式编程主要通过预言可能出现的缺陷来实现容错性。使用 actor 模型并不需要使用防御式编程,而是遵循“任其崩溃”的哲学,让 actor 的上层管理者来处理这些问题。这样做的优势在于:
相比已经介绍过的并发模型,actor 模型的一个重大优点是它支持分布式——它可以将消息发送到另外一台计算机,就像发送到本地计算机上的 actor 一样。这被称为地理位置透明。
上面演示的代码过于底层,而 OTP 为使用 Actor 模型提供更多工具。
主要概念包括:
Actor 除了优点,也会带来它独有的一些问题。
Actor 模型并没有直接提供并行支持,事实上可以自己构造,但由于 actor 之间不共享状态,仅通过消息传递进行交流会不太适合实施细粒度的并行。
个人认为应用 Actor 模型的最大障碍是开发者们的思维方式转变,尤其对于一个以 Java 这种命令式语言作为生产语言的团队来说。
另外,如果想要深入理解 Actor 模型,可以直接参考 Erlang/OTP,如果想要在生产中构建基于 Actor 模型的项目,推荐使用运行于 JVM 的 Akka,当然,如果同时使用 Scala 语言就更好了,因为 Java 中一贯的编程思路如共享可变状态、对象可变等不利于使用 Actor 模型。