Actors in Scala(Scala中的Actor)(预打印版) 第一章 Concurrency Everywhere (B)
2011.10.01
Actor除了让你关注并发应用的扩展性方面之外,Actor对并发性方面更高视角的描述对大家也非常有帮助,这是因为它提供了理解并发程序如何工作的更逼真的抽象。特别的,当程序为了利用并发特性而从头开始重新设计时,并发程序展示了两个特征,当然这两个特征也存在于顺序应用(sequential applications)中。为了看看这两个特征到底是什么,我们暂停一下先看看办公室中苏达机的工作过程。
苏达机很方便,不但是因为在炎热的夏日它能为我们制作解渴的饮料,而且也因为它能很好的类比从一个完好状态转换到其他状态的程序。在开始工作前,苏达机等候用户输入,比如提示用户插入硬币。插入硬币后,苏达机进入新的状态,询问用户,让其选择所需的饮料,用户选择后,苏达机便给用户分配一听饮料,然后苏达机又回到初始状态。偶尔情况下,苏达机可能会用光了饮料罐,此时苏达机进入“暂停服务”的状态。
在任何时候,苏达机仅处于一种状态。这种状态对于苏达机来说是全局的。苏达机的每个组件:硬币接收设备、显示单元、选择输入面板、饮料罐分发器等等都必须查询苏达机的全局状态以决定下一个行为。比如:如果机器已经处在用户选择完毕的状态下,饮料罐分发器才允许发送苏达饮料到输出货盘中。
除了允许处于明确定义的状态中之外,我们的抽象化设计建议另外两个苏达机特性:首先,苏达机可进入的可能的状态数量是有限的。其次,给定其中任何一个可能的状态,我们可以提前预知下一个状态是什么。比如如果你插入足够数量的硬币,那么将预期下一步将被询问选择何种饮料。如果选择了之后,那么预期将会在货盘中得到所选择的饮料。
当然,你可能有机会碰到苏达机没有精确按照这样可预测的、确定的方式运行。你插入的大量的硬币,显示单元并没有提示你选择饮料,而提示了不受欢迎的“已售完”的消息。无论你如何使劲得砸这台机器,你可能不但没有收到任何提示消息,而且也没有收到任何冰冷的点心。现实世界的经验告诉我们,苏达机就像大多数其他的物理机器一样,都不是完全的确定的。大多数时候,他们会从一种定义完好的状态以期望的,预设的方式转换到另外一个状态;但是偶尔他们会以一种事先不可预测的方式从一种状态转换到错误的状态。
因此,一个更实际的苏达机模型应该包含一些不确定的属性:这个模型应该使得苏达机具有在不同状态间转换时以一种不必提前预知下一状态的方式运行的能力。
虽然我们平常擅长处理这种物理对象的不确定性,如同与人打交道一样,但是当我们在软件中遇到这种不确定性问题时,我们总把这种行为归为“bug”。为了查找这样的bug我们需要钻进代码中仔细研究,因为我们程序没有充分的处理某些方面的逻辑。
很自然的,作为开发者,我们希望开发出定义完整的程序,因此程序行为应该都是期望的,即程序严格遵照详细周密的文档所定义的行为。确实,为程序编写测试用例是保证程序能按照文档要求的行为运行的一种方法。
然而,并发程序不像是确定性序列代码,而更象苏达机。由于并发系统内部某些方面本身没有详细指定,因此并发程序从中获取了高性能。
原因从直觉上也非常容易理解,我们考虑一个四个核心的CPU:假设运行在第一个核心上的程序发消息给其他的三个核心,然后等待其他三个核心的响应,一旦收到了响应,第一个核心就会接着处理响应的消息。
在实践中,核心1,2,3发送返回响应的顺序由这三个核心完成计算任务的顺序决定。如果响应的顺序没有明确指定,那么核心1只要收到了任何一个响应就可以开始处理,而不必等待那个最慢的核心处理完毕。
在这个例子中,不去明确指定核心2,3,4的响应顺序能够帮助CPU最大化使用计算资源。与此同时,你的程序再也不必依赖任何指定的消息顺序。取而代之的是,你的应用程序组件计算过程或者程序组件之间的交互即使没有完全指定,你的应用程序也应该能够确定的运行。
从不确定的组件计算中建立确定性的系统应用都是以数据为中心的“商品”,即现成组件(commodity off-the- shelf (COTS) components)。许多著名的web服务公司都已经证明了COTS硬件(现成商品的硬件)作为建立高可靠性数据中心模块的经济性。当基础软件缓解了开发者对于在纷繁复杂的不同的硬件组件的数据中心间如何分区工作的焦虑之后, 这样的环境变得非常实际。取而代之的是应用程序开发者关注更高级别的事情,比如指定当处理请求时使用的算法等。
一个非常流行的,使得程序能够在COTS集群中运行的例子是Map-Reduce。使用了Map-Reduce,用户提供一些数据和一些处理数据的算法,并且提交数据和算法给Map-Reduce基础软件,Map-Reduce基础软件依次分发这些计算工作任务给集群中的可用节点,然后将最终结果返回给用户。
Map-Reduce的很重要的一方面就是当用户提交了工作任务之后,用户会合情合理的期待一些结果返回。比如,一个运行Map-Reduce任务的节点没有在指定时间内返回结果,Map-Reduce基础软件就会重新在其他可用结点上启动这个计算任务。因为它(Map-Reduce基础软件)保证会返回结果,Map-Reduce不仅允许这个架构能够扩展运算密集型的任务到一个集群上,而且更重要的是Map-Reduce保证了可靠计算。正是这种可靠性,使得Map-Reduce适合于在基于COTS的计算机上运行。
使用Map-Reduce的开发者能够期望收到一个返回结果,当然这个结果是在提交计算任务前不知道的。用户仅仅知道会收到一个结果,但是他不会提前知道结果是什么。更通常的说,系统提供了保证,保证在某个时间点计算会完成,但是使用系统的开发者不能提前设置一个计算将会运行多久的时间界限。
直观的,非常容易理解这样的事实:当基础软件分区了计算任务,基础软件必须同其他系统组件通信,比如它必须发送消息给集群内其他节点,并且等待响应。这种通信会有各种各样的延迟,这些通信延迟影响了返回结果的时间。但是在提交任务之间,你并不会知道这样的通信延迟到底有多大。
虽然一些Map-Reduce的实现目标是确保任务在一定时间内返回一些结果,即使也许是未完成的结果,但是Actor的计算模型更通用:它承认我们不能提前知道一个并行计算将会执行多久。换句话说,在运行并行计算前,你不能提前设置一个时间长度期限。与传统的相比,顺序算法对于给定的输入,建模给出明确的计算时间。
通过承认没有计算时间界限的属性,Actor力争提供更实际的并发计算模型。虽然不同的通信延迟在分布式系统或者集群中很容易掌握,但是在一个四核CPU中也不可能提前知道核心1,2,3需要多久将响应返回给核心1。我们所能能说的是响应最终总会到达。
同时,没有时间边界并不意味着需要无限的时间:无限是一个有趣的概念,它为现实的建模计算提供了有限的用途。Actor模型确实需要并行计算在有限的时间内结束,但是它承认在计算之前它不可能知道需要多久计算完毕。
在Actor模型中,无边界性和不确定性是并发计算的关键属性。然而这些属性也存在于主要的顺序计算的系统中,他们普遍存在于并发程序中。承认这些并发的属性并且提供一个模型给开发者,允许开发者面对这些属性时合理推理并发程序是Actor模型的主要目标。Actor模型通过提供一些令人吃惊的简单抽象完成了这些目标,这些简单抽象能够表达之前在顺序程序中你所熟悉诸如if,while,for等的顺序程序的控制结构,并且让这些控制结构在随时都可能扩展的并发系统中可预测的正常工作。