Actors in Scala(Scala中的Actor)(预打印版) 第一章 Concurrency Everywhere (A)
2011.09.29
写在原文开始翻译之前的一些琐事。
本人在2003年结识了Java,在Java世界中摸爬滚打了6年后,从2009年开始学习Scala。最近2年使用Scala设计、开发并上线了股票衍生数据实时处理平台,效果颇好。回首2年来使用Scala,曾经遇到过非常多的困难和疑惑,好在本人的英文水平还不错,这些问题大多数能在StackOverflow、Scala Reference、Programming In Scala和Scala的源代码中找到答案。最近又在浏览Actors in Scala,发现其中对于Actor的介绍颇深,受益匪浅,不敢独享,因此翻译写出来与大家分享,给那些想深入学习Scala但是英文水平一般弟兄们一点帮助。我会在翻译中加入自己的一些理解,全当读书笔记了。如果有不恰当的地方,请及时指出,以免误导更多的人,谢谢大家。
Actor的并发模型诞生于实际的需求:70年代当Carl Hewitt和他的团队在麻省理工大学首次提出actor的模型的时候,当时计算机的计算速度已经比较慢了。然而当时已经可以做到将一大块工作分解到多台计算机上并且让他们并行计算。可是Hewitt的团队想设计一种能够非常简单的建立并发系统的模型,而且使用者还可以使用这个模型推理并发系统的行为。他们确信基于这样的模型推理,便可以使得开发者对他们所开发的并发程序的行为更有把握。
虽然基于Actor的并发模型自从诞生以来一直是一个非常重要的概念,但是直到最近这个概念才被广泛接收,这在某种程度上也是因为截止目前为止也没有出现一门广泛使用的,并且对Actor提供良好支持的语言。一个有效的Actor模型的实现,往往给宿主语言带来的巨大的负担,因此主流语言几乎都无法胜任这个任务。此时Scala出现了并接收了这个挑战,并且基于JVM实现了Actor并发模型的全部特性。因为Scala代码可以无缝与Java源代码、Java类库或者其他的JVM语言互操作,所以Scala的Actor模型提供了一种激动人心的实际方式去构建一个可扩展的、可靠的并发程序。
就像其他有影响力的概念一样,Actor模型可以在多个层次理解和使用。在第一个层次上,基于Actor的编程模型提供了一种能在独立运行的线程或者进程间交换消息的简单方法。在另外一个层次上,Actor模型通常使得并发编程更简单,因为Actor模型使得开发者更专注于高层次的并发抽象,使得开发者从纷繁复杂的、易出错的底层细节中解脱出来。在更高的层次上,Actor模型是在一个并发是常态而非异常的世界中构建可靠程序的快速方法。
本书着眼于解释使用Scala在以上这三个层次上进行基于Actor的编程。在进入Actor的细节前,先后退一步,我们把Actor和其他的并发编程方法比较一下,这些方法你也许已经比较熟悉。
在过去几十年中,主流的计算架构都是着眼于如何使得单线程的指令序列执行的更快。这就导致了对于应用程序计算性能的摩尔定律:在过去的二十年中,单位成本的处理器性能大约每18个月会翻一倍,并且开发者依靠这些处理性能的增长趋势来确保他们开发的越来越复杂的程序会执行得更快。
摩尔定律已经非常精确的预测了处理器性能的增长,并且在过去的20年中,期望处理器的计算能力每一年半翻倍一次也非常合理。这些年,为了使得这样的增长趋势更实际,芯片设计师不得不在他们的设计方向上做了重大转换。目前不再试图再去提高专注于执行单线程指令的CPU时钟周期,而是在新的设计中尽可能在单个CPU芯片上执行多个并发线程。在未来几年中,在一块芯片上的每个计算核心的时钟速度的提高余地将会非常小,而普通服务器已经具有了多个内核的CPU,多核芯片在廉价的桌面电脑和笔记本电脑也越来越平常。
诸如Inter X86这样普通处理器架构设计上的巨大转变,给开发者(的思路)带来了两个分支。首先,由于单个内核的时钟频率增加越来越小,我们将不得不更加注意序列代码的算法效率。其次,这一点在Actor模型中更重要,那就是我们需要设计出能够尽可能利用可用CPU内核的程序。换句话说,我们不但要写出能够在支持并发的硬件上正确执行的程序,而且要设计出能够方便扩展到所有可用CPU内核上执行的程序。
在并发程序中,许多独立执行的线程或独立执行的处理序列一起工作,来履行应用程序的需求。仔细调查并发程序,你会发现并发程序绝大多数时候都在定义“并行执行的序列处理将如何通信”,这样做的目的是为了使得并行程序执行变得可预测。
在并行线程间通信的两个最常用的方法是对共享状态的“同步”和消息传输。许多熟悉的程序结构,比如semaphores and monitors(信号和监视器)都是基于共享状态同步的。并行程序的开发者应该对这两个结构都非常熟悉,比如Java程序员可以在java.util.concurrent库中找到这些结构。任何人使用共享状态同步的最大挑战就是如何避免并发危害,比如数据竞争、死锁和平行扩展。
消息传输是多个协作线程间通信的替代方式。有两种重要类型的系统是基于消息传输的。在基于通道(channel-based)的系统中,消息被发送到进程可以共享的通道或者端口上,而进程可以从相同的通道或者端口上接收消息。比如基于通道的系统MPI(Message-Passing Interface)和基于CSP(Communicating sequential processes)范式的系统,比如Go语言。基于Actor的系统(或者代理、或者Erlang风格的处理)是消息传输并发的第二种。在这些系统中消息直接发送给Actor,而没有必要在进程间创建中介通道。
消息传输对应于共享状态并发更重要的优点是消息传输更容易避免数据竞争。如果进程间仅仅通过消息传输通信,并且这些消息都是immutable(不可更改的),那么竞争就从设计上消除了。此外,许多证据建议,在实际中这种方式能够减少死锁。消息传输的一个潜在的缺点是通信的开销比较高。为了通信,进程不得不创建和发送消息,并且这些消息在被取走去异步通信前,经常被缓存在队列中。相反,共享状态并发使得线程直接访问共享内存,只要共享内存被正常同步了。为了减小消息通信的开销,大消息不应该通过拷贝消息状态的方式传输,相反仅仅传输消息的引用。然而当多个线程同时访问相同的可更改的消息(资源)时,又再次引入了数据竞争。这是一项正在进行的静态检查器(比如Scala的编译器为独特类型提供的插件)的研究工作,静态检查器能够验证通过引用传输的可更改消息的程序不包含数据竞争。
让我们退一步从更高的视角来看看基于Actor的编程。为了比较Actor模型与传统的并发设计的区别和联系,我们来快速看一下当地铁路战场的工作情况。
设想一下,你站在一座大桥上俯瞰多条单独的轨道进入铁路站场。你能观察到许多看起来似乎是相互独立的行为在发生,比如火车到达、小轿车被装载、卸载等等。
假设你的工作是要设计铁路站场。想想诸如线程、锁、监视器等这些术语与解决在铁路站场中火车在并行的铁轨上运行而不冲撞是多么相似。这是一个非常重要的要求,没有这点要求铁路站场将会变成非常危险的地方。为了完成这个任务,你可以使用一些特别的工具,比如semaphores(信号量), monitors(监视器), switches(交换器)。
Actor模型从更高角度阐述了相同的铁路站场,它确保所有的并发行为在铁路站场中平稳运行:所有的运载车辆都能找到前往运货车厢的路,所有的列车都能在轨道上平稳前进,所有的行为都得到合适的协调。
在设计铁路站场时你需要两个视角:想想单独铁轨上的较低视角,在这个视角中,需要确保火车不会故意越轨;想想整个火车站场设施的视角,在这个视角中,要确保你设计的铁路站场总体上平稳运营,并且铁路站场如果有需要的话可以扩展以容纳未来增长的交通流量。简单的添加新的铁轨不足以解决问题:你需要一些整体的设计规则以确保整个铁路站场能够处理增长的交通流量,并且如果有更大的交通流量的话能够扩展使用全部轨道的运力。
对相对底层的单独轨道的细节(或者说线程间通信相关的问题)的研究与另一方面更高层次的全部铁路站场设施(Actor模型)的研究需要不同的技能和经验。基于Actor的系统通常使用线程、监视器、锁等实现,但是Actor隐藏了那些底层的细节,使得你可以从更高的有利位置来思考并发程序。