讨论Actor模型,首先要树立两个概念,分布式+并发,很顾名思义地,分布式会带来资源隔离、系统差异、拓扑互联、传输效率等一系列问题,而并发会带来不确定性、同步和异步通信,所以我们需要彻底放弃一些固有思想,才能更好地理解Actor。
共享内存
共享内存就是可以由多个程序同时访问的内存,用于在进程间通信,是传统的并发编程一种常见的实现方式。并发会导致竞争,解决竞争的方式很直观地就是使用锁等同步原语规定执行顺序。共享内存是一种简单的并发模式,也有很多问题(比如锁、比如死锁,比如分布式系统,blablabla),所以,需要更高效、更简单、更实用的并发模型。
消息传递
基于消息的并发模型正在被更多的尝试,比较典型的有CSP(communicating sequential process)和Actor。
消息传递是一种“通过传递消息触发行为”的软件编程范式,是一些并发模型和面向对象编程的内核,常用作组成程序的对象之间相互协作的一种方式,或跨机运行的对象与系统之间的互动手段。
和直接通过名字调用的方式不同,调用程序向处理程序发送消息,处理程序可能是一个对象也可能是Actor,会实现特定的对象模型或是处理方式响应接收到的消息。为了便于“封装”和“分发”,处理消息的部分通常会单独实现为中间层,承担:
- 在消息来源的不同位置使用不同的操作系统和编程语言查找进程
- 如果处理消息的适当对象当前未运行,则将消息保存在队列中,然后在对象可用时调用消息。 此外,如果需要,存储结果,直到发送对象准备好接收它
- 控制分布式事务系统的要求(由语句封装的序列代码段,作为原子操作),例如原子性、一致性、隔离性、持久性 (ACID) 测试数据
分布式的消息传递可以作为并行模型的框架,依托于不同的消息传递方式,子系统之间可以实现在时间和空间上解耦。
整体上,消息传递的方式分为同步传递和异步传递。同步传递和同步调用函数类似,在一些面向对象的编程语言中可以看到实例,通常发生在同时发生的不同对象之间。
异步消息传递,消息被发送到消息队列而无需等待响应返回,接收进程在空闲时间会处理消息然后将结果返回给队列以供发送端拾取。因此,为了实现异步消息传递,通常需要额外的功能来存储或重新传递数据,这些功能常由中间件处理,一种常见的类型是面向消息的中间件 (MOM)。异步通信在缓冲区填满时可能会存在死锁或可靠性的问题。
除了单纯的同步/异步模式,也存在混合模式,包括在异步框架上做同步调度,或基于同步系统做异步实现。例如微内核只提供了同步原语,因此可以使用辅助线程实现异步消息传递。
分布式对象系统
消息传递系统更多用于分布式对象,消息处理程序可能处理来自多个来源的消息,这意味着在方法调用期间状态并不可控,消息传递抽象通常会隐藏状态的改变,因此分布式对象系统被称为“无共享”系统。对于分布式对象,通常需要总线层负责跨系统的数据发送、转换和接收。对于这种类型的消息传递,程序语言系统可以被包装或处理为能够发送和接收的消息的大粒度对象。
传统的过程调用中参数通常不需要额外空间存储或时间传输,但分布式系统由于子系统使用单独的地址空间,无法进行地址传递;而在消息传递中,参数又必须通过消息传递。因此与传统的调用过程相比,分布式或异步消息传递会引入额外的开销。
数学建模
常用的消息传递的数学模型有Actor模型和π演算,在数学推到中消息是唯一转移控制权的方式。
简单来讲,π演算就是一种类似于λ演算的过程演算,对通信过程的数学语义形式化建模,对过程表达式应用数学规则,目的是推导并发过程的运行/交互/行为/迁移/等价等状态。与其他演算不同的是,π演算中以channel作为数据,可以通过其他channel传递,因此允许表达进程迁移。
动作和过程表达是π演算语法的核心,在基础定义中用小写字母充当通信端口、变量和数据值,大写字母P,Q...指代一个过程,过程的集合用“A”表示,在集合中每个过程用非负的序号区分。根据对通信的抽象,动作前缀分为“Output”和“Input”,Output指沿着信道a发送一个name x,可以认为是输出端,x是从该端口发出的数据;Input标识沿着信道a接收一个name,为了表达的完备性,用Silent表示外部不可见的内部动作。过程表达式的基本范式是
,指过程P必须在动作前缀
完成后执行。对不同过程之间的关系,定义了与其他演算相同含义的“Nil”、“Sum”和“Parallel”,其中Nil代表终止,Sum表示“逻辑或”,Parallel表示并行。"Match"和“Mismatch”是对布尔类型的简单抽象,只用于判断传输内容是否一致。“Restriction“用于描述私有数据,在
中x仅在P内部可用。
CSP
Golang中的“chan”就是CSP的典型实例。CSP中每个工作节点相互不连接,而是通过Channel进行通信。以“chan”为例,只有Channel的两个端点:发送方和接收方都准备好时,消息才会传递,否则将一直等待;对于有一定缓冲区的chan,消息可以暂存在中介,但实际上原理类似。(对CSP感兴趣可以出门右转到Communicating Sequential Processes,前面的语法规约是一定要看的,后面章节可以各取所需)。
Actor模型
Actor模型是一种基于消息传递的并发计算数学模型,一个非常Conceptual的理论模型,它不关心实现、不考虑低级机器术语,只对并行作高级的抽象。Actor模型中没有全局状态,具有无界不确定性,由于“确定性”和“不确定性”之间的差异,Actor模型作为计算模型具有根本性的改变,同时这些差异也会导致Actor模型有很多“不可理喻”的地方。除了收到一些并发编程语言和非确定性计算模型的影响,按照Hewitt的说法,Actor模型受到了包括广义相对论、量子力学等物理定理的启发。“忠于物理”,Actor模型将消息和发送者解耦;支持单向异步通信;支持直接通信,消息发出后由接收方负责,不通过任何中间介质(和CSP明显不同);而同时,Actor模型没有完美的代数推导(hmm,Magic!)
Actor model wanted to remain faithful to physics which says you don't put an intermediary because it is a source, a necessary source.
Actor的集合以及可能存在的功能组件共同组成Actor系统。类似于“One ant is no ant","One Actor is no Actor",当Actor系统中存在多个Actor对象时,消息才能传递;反过来,“Actor”是并发计算的通用原语,Everything is Actor,例如可以创建一个Actor用作channel。Actor的设计基于行为,而不是基于面向对象的“类”,因此Actor不提供任何访问、调用和被修改状态的接口,方法的实现也不能继承,唯一和外界的路由就是消息。每个消息最多只会传递一次,Actor中消息要包含传递目标地址,地址并不作为Actor对象的标识,也就是说即使地址相同也不代表是同一个Actor,除了地址和消息内容,也可以将控制结构作为消息传递。收到消息时,Actor可以有三种行为,这些行为理论上也是可以并行的,
- 创建有限数量的Actor
- 向已知地址的Actor发送有限数量的消息
- 决定如何响应下一条消息,不同于“continuation”的串行执行,只是决定如何处理
理论是每个Actor一次只处理一个消息,Actor内部最多有一个线程处于活跃状态,当同一个Actor被同时调用时,会导致串行,因此在特定的情况下需要实现做好调度。
Conceptually one message at one time and Implementation make it more concurrent.
和其他基于消息传递的并行模型相比,Actor模型消除了对线程、消息顺序到达和垃圾收集(自动存储管理)的限制,因此如果对Actor系统施加这些限制就会降低它的性能和可扩展性:
- 单线程:对线程的使用没有限制
- 消息传递顺序:不限制消息到达顺序
- 消息和发送者解耦:消息的语法独立于发送者
- 没有垃圾回收
特点
- 并发,Actor之间并发,Actor响应消息的行为并发
- 消息,消息中包含Actor地址,也可以将控制结构作为消息传递
- 动态,可以动态创建Actor,构成可变的拓扑结构
- 直连,仅通过直接通信和异步消息传递进行交互,它只能从直连的其他Actors获取信息,直连的方式包括:物理直连/内存或硬盘地址/网络地址/Email地址
- 局部,处理消息时只能将消息发送到在处理消息时创建、或在收到消息之前已知、或通过消息接收到的地址
- 不确定性,不受消息到达顺序的限制,只要行为的顺序不影响未来行为,就认为这些操作是“准交换的”,准交换性在一定范围内可以限制不确定性
- 组合地址
其中,动态的拓扑结构可能为实现者提供了更大的调度自由度。消息传递中一个常见的流程是Sender向Recipient发送消息,并期望从Recipient接收响应。适配到Actor模型中,一个可能的流程是:1. Sender Actor向Recipient Actor发送消息,消息包含消息内容和另一个Customer Actor的地址;2. Recipient Actor处理消息,并将响应发送给Customer Actor。但由于Actor可以动态创建,整个过程可能会有更多的Actor参与进来,合理地管理这些Actor可能能够带来更高的并行度。
通过直接通信和异步更便于实现模块化。但Actor的数量与硬件资源的数量没有必然关系,应当可以以任何符合Actor规则的方式自由使用硬件资源。
上面说过,Actor中的地址并不是Actor对象的标识,因为Actor允许组合地址,在一些情况下要阻止地址的组合,否则可能会导致不可预测的结果。
由于缺乏同步性,Actor模型的安全性天然存在隐患。通常采用更严格的方式来保证安全性:
- 物理连接的硬连线
- 标记内存,类似Lisp机制
- 虚拟机,像Java VM,通用语言运行时等
- 对Actor及其地址进行签名和/或加密
计算表示定理
Actor系统中有一个计算表示定理用来数学表征封闭系统的所有可能计算的方法,封闭系统表示不接收任何外部通信的系统。在这样的系统中,如果初始行为是有效的,那么系统的可能输出是递归可枚举的(邱奇理论的推广)
Enumeration Theorem: If the primitive Actors of a closed Actor System S are effective, then the possible outputs of S are recursively enumerable.
ActorScript
ActorScript是一种可以实现自适应并发的编程语言,尝试用最少的原语实现最高层级的性能、可扩展性和表达能力。自适应并发是一种表达计算的能力,可以根据需求和可用容量进行调整以适应可用资源。可以按照下面的方式表示自适应并发:
- 表达
- 语言中的一切都用消息传递完成,包括语言本身的定义
- 直接表达任意的推拉并发
- 功能和逻辑编程集成到通用并发编程中
- 程序不暴露低级实现机制,如线程、任务、锁、内核等。消息可以直接通信,无需通过代理、类层次结构、通道、邮箱、管道、端口、队列等间接传递
- 使用sponsors对能源、处理、存储和通信进行计划资源控制
- 以二进制XML和JSON为数据类型
- 应用程序不能直接互相影响
- 性能
- 允许执行根据系统复杂和容量动态调整
- 关键路径上的最小延迟
非确定性计算模型
几种影响了Actor的非确定性计算模型。
Concurrency versus Turing's Model
图灵模型中计算被认为是由一个设备在一个单一的位置,从一个明确定义的状态前进到下一个状态,可以形式化表示为:
- 有界性,有限数量的配置
- 局部性,可以更改立即可识别的配置
相比之下,Actor模型中计算被认为是分布在计算设备异步通信的空间中的,并且整个计算不处于任何明确定义的状态。Actor模型具有局部性和局限有限性的属性:
- 局部性,Actor只能更改本地存储,该本地存储只能包含创建时提供的Actor地址以及在消息中接收到的地址
- 局部有限性,在任何一个点一个Actor具有有限存储,响应信息时只能发送有限数量的信息或创建有限数量的Actors
因此,Actor系统可以执行图灵机无法执行的计算,例如图灵机的空白磁带只能计算有限界的整数,而Actor收到Stop时会在本地存储中以无界的数字停止。
-Calculus
演算可以视作最早期的消息传递编程语言,它的本质就是变量绑定和替换。纯粹的
演算只用“术语”和“变量 ”构建,并应用
变换、
规约、
变换等语法规则进行演算(讲解比较通俗的λ演算)。
但替代模型不适合并发,因为它不允许共享变化的资源的能力;Lisp解释器使用一种叫“环境”的数据结构让参数值不用替代到λ表达式,这虽然解决了共享资源的问题,但不支持并行。
而Actor模型认为所有由术语表示或通过名称访问的事物(值、寄存器、运算符、进程、对象...)都是过程,都可以抽象为同一种东西Actor,这里暗含了表达的同质性和完整性。
Petri nets
提到非确定性计算,一定绕不开Petri nets。Petri网也是一个受到物理启发的模型,它的本质就是“A game of tokens”。在表达能力上,Petri网只能对控制流建模而不能对数据流建模;从执行机制上它需要同时动作,Petri网中计算的原子步骤是转换,因此tokens要同时从转换的输入位置消失并出现在输出位置。而Actor模型中,并不会存在多个位置状态的同时改变。
Simula
Simula使用全局时钟模拟非确定性离散事件。Simula中的Station和Actor类似,每个Station由一个程序控制并持有一个Customer队列,Station可以创建或丢弃Customer。在时钟的下一个时间点,下一个要模拟的Station程序将更新变量。为了提高模块化,Simula67将对象组织成具有子类的类,子类可以继承超类的方法,每个对象描述了一部分事件序列。因为是模拟机制,所以实际上是一种非确定性顺序执行的形式,对象充当“准并行”的进程,通过控制权的转移实现准并行排序。因此Simula具有不确定性,但没有并发性。
实现
Actor的消息是点对点具名发送的,因此可能不需要复杂的消息系统,反而更关注如何处理消息。
Cosmic Cube
Cosmic Cube是一种多指令多数据的并行处理器,使用消息传递而非共享变量进行并发进程之间的通信,为Actor系统提供了架构支持。
J-Machine
J-Machine包括,
- 异步消息传递
- 一个统一的Actor地址空间,无论接收者Actor是本地的还是远程的,都可以同时向其发送消息
- 开发了一种 Actor流水线
π演算
结合上面对π演算和Actor的介绍可以看到区别,
π演算 | Actor | |
进程数 | 固定数量 | 动态变化 |
消息内容 | 整数和字符串 | 允许消息中传递Actor的地址 |
拓扑结构 | 固定拓扑 | 动态拓扑 |
通信方式 | 同步通信 | 允许异步通信 |
不确定性 | 有限的不确定性 | 无界不确定性 |
Actor编程语言原型
通过将每个程序构造定义为具有自己行为的Actor来定义语义;通过在程序构造之间传递Eval消息来建模行为。
To Be Continue...