共计 3325 个字符,预计需要花费 9 分钟才能阅读完成。
写在开始
一般来说有两种策略用来在并发线程中进行通信:共享数据和消息传递。应用共享数据形式的并发编程面临的最大的一个问题就是数据条件竞争。解决各种锁的问题是让人非常头痛的一件事。
传统少数风行的语言并发是基于多线程之间的共享内存,应用同步办法避免写抢夺,Actors 应用音讯模型,每个 Actor 在同一时间解决最多一个音讯,能够发送音讯给其余 Actor,保障了独自写准则。从而奇妙防止了多线程写抢夺。和共享数据形式相比,消息传递机制最大的长处就是不会产生数据竞争状态。实现消息传递有两种常见的类型:基于 channel(golang 为典型代表)的消息传递和基于 Actor(erlang 为代表)的消息传递。
Actor 简介
Actor 模型 (Actor model) 首先是由 Carl Hewitt 在 1973 定义,由 Erlang OTP 推广,其 消息传递更加合乎面向对象的原始用意。Actor 属于并发组件模型,通过组件形式定义并发编程范式的高级阶段,防止使用者间接接触多线程并发或线程池等根底概念。
Actor 模型 = 数据 + 行为 + 音讯。
Actor 模型是一个通用的并发编程模型,而非某个语言或框架所有,简直能够用在任何一门编程语言中,其中最典型的是 erlang,在语言层面就提供了 Actor 模型的反对,杀手锏利用 RabbitMQ 就是基于 erlang 开发的。
更加面向对象
Actor 相似面向对象编程(OO)中的对象,每个 Actor 实例封装了本人相干的状态,并且和其余 Actor 处于物理隔离状态。举个游戏玩家的例子,每个玩家在 Actor 零碎中是 Player 这个 Actor 的一个实例,每个 player 都有本人的属性,比方 Id,昵称,攻击力等,体现到代码级别其实和咱们 OO 的代码并无多大区别,在零碎内存级别也是呈现了多个 OO 的实例
class PlayerActor
{public int Id { get; set;}
public string Name {get; set;}
}
无锁
在应用 Java/C# 等语言进行并发编程时须要特地的关注锁和内存原子性等一系列线程问题,而 Actor 模型外部的状态由它本人保护即它外部数据只能由它本人批改(通过消息传递来进行状态批改),所以应用 Actors 模型进行并发编程能够很好地防止这些问题。Actor 外部是以单线程的模式来执行的,相似于 redis,所以 Actor 齐全能够实现分布式锁相似的利用。
异步
每个 Actor 都有一个专用的 MailBox 来接管音讯,这也是 Actor 实现异步的根底。当一个 Actor 实例向另外一个 Actor 发消息的时候,并非间接调用 Actor 的办法,而是把消息传递到对应的 MailBox 里,就如同邮递员,并不是把邮件间接送到收信人手里,而是放进每家的邮箱,这样邮递员就能够疾速的进行下一项工作。所以在 Actor 零碎里,Actor 发送一条音讯是十分快的。
这样的设计次要劣势就是解耦了 Actor,数万个 Actor 并发的运行,每个 actor 都以本人的步调运行,且发送音讯,接管音讯都不会被阻塞。
隔离
每个 Actor 的实例都保护这本人的状态,与其余 Actor 实例处于物理隔离状态,并非像 多线程 + 锁 模式那样基于共享数据。Actor 通过音讯的模式与其余 Actor 进行通信,与 OO 式的消息传递形式不同,Actor 之间音讯的传递是真正物理上的消息传递。
天生分布式
每个 Actor 实例的地位通明,无论 Actor 地址是在本地还是在近程机器上对于代码来说都是一样的。每个 Actor 的实例十分小,最多几百字节,所以单机几十万的 Actor 的实例很轻松。如果你写过 golang 代码,就会发现其实 Actor 在重量级上很像 Goroutine。因为地位透明性,所以 Actor 零碎能够随便的横向扩大来应答并发,对于调用者来说,调用的 Actor 的地位就在本地,当然这也得益于 Actor 零碎弱小的路由零碎。
生命周期
每个 Actor 实例都有本人的生命周期,就像 C# java 中的 GC 机制一样,对于须要淘汰的 Actor,零碎会销毁而后开释内存等资源来保证系统的持续性。其实在 Actor 零碎中,Actor 的销毁齐全能够手动干涉,或者做到零碎自动化销毁。
容错
说到 Actor 的容错,不得不说还是挺令人意外的。传统的编程形式都是在未来可能出现异常的中央去捕捉异样来保证系统的稳定性,这就是所谓的进攻式编程。然而进攻式编程也有本人的毛病,相似于事实,进攻的一方永远不能 100% 的进攻住所有未来可能呈现代码缺点的中央。比方在 java 代码中很多中央充斥着判断变量是否为 nil,这些就属于进攻式编码最典型的案例。然而 Actor 模型的程序并不进行进攻式编程,而是遵循“任其解体”的哲学,让 Actor 的管理者们来解决这些解体问题。比方一个 Actor 解体之后,管理者能够抉择创立新的实例或者记录日志。每个 Actor 的解体或者异样信息都能够反馈到管理者那里,这就保障了 Actor 零碎在治理每个 Actor 实例的灵活性。
劣势
天下无完满的语言,框架 / 模型亦是如此。Actor 作为分布式下并发模型的一种,也有其劣势。
- 因为同一类型的 Actor 对象是扩散在多个宿主之中,所以取多个 Actor 的汇合是个软肋。比方在电商零碎中,商品作为一类 Actor,查问一个商品的列表在少数状况下通过以下过程:首先依据查问条件筛选出一系列商品 id,依据商品 id 别离取商品 Actor 列表(很可能会产生一个商品搜寻的服务,无论是用 es 或者其余搜索引擎)。如果量十分大的话,有产生网络风暴的危险(尽管几率十分小)。在实时性要求不是太高的状况下,其实也能够独立进去商品 Actor 的列表,利用 MQ 接管商品信息批改的信号来解决数据一致性的问题。
- 在很多状况下基于 Actor 模型的分布式系统,缓存很有可能是过程内缓存,也就是说每个 Actor 其实都在过程内保留了本人的状态信息,业内通常把这种服务成为有状态服务。然而每个 Actor 又有本人的生命周期,会产生问题吗?呵呵,兴许吧。想想一下,还是拿商品作为例子,如果环境是非 Actor 并发模型,商品的缓存能够利用 LRU 策略来淘汰非沉闷的商品缓存,来保障内存不会应用适量,如果是基于 Actor 模型的过程内缓存呢,每个 actor 其实就是缓存自身,就不那么容易利用 LRU 策略来保障内存使用量了,因为 Actor 的沉闷状态对于你来说是未知的。
- 分布式事物问题,其实这是所有分布式模型都面临的问题,非因为 Actor 而存在。还是以商品 Actor 为例,增加一个商品的时候,商品 Actor 和统计商品的 Actor(很多状况下的确被设计为两类 Actor 服务)须要保障事物的完整性,数据的一致性。在很多的状况下能够就义实时一致性用最终一致性来保障。
- 每个 Actor 的 mailBox 有可能会呈现沉积或者满的状况,当这种状况产生,新音讯的解决形式是被摈弃还是期待呢,所以当设计一个 Actor 零碎的时候 mailBox 的设计须要留神。
升华一下
- 通过以上介绍,既然 Actor 对于地位是通明的,任何 Actor 对于其余 Actor 就如同在本地一样。基于这个个性咱们能够做很多事件了,以前传统的分布式系统,A 服务器如果想和 B 服务器通信,要么 RPC 的调用(http 调用不太罕用),要么通过 MQ 零碎。然而在 Actor 零碎中,服务器之间的通信都变的很优雅了,尽管实质上也属于 RPC 调用,然而对于编码者来说就如同在调用本地函数一样。其实当初比拟时髦的是 Streaming 形式。
- 因为 Actor 零碎的执行模型是单线程,并且异步,所以但凡有资源竞争的相似性能都非常适合 Actor 模型,比方秒杀流动。
- 基于以上的介绍,Actor 模型在设计层面天生就反对了负载平衡,而且对于程度扩容反对的十分好。当然 Actor 的分布式系统也是须要服务注册核心的。
- 尽管 Actor 是单线程执行模型,并不意味着每个 Actor 都须要占用一个线程,其实 Actor 上执行的工作就像 Golang 的 goroutine 一样,齐全能够是一个轻量级的货色,而且一个宿主上所有的 Actor 能够共享一个线程池,这就保障了在应用起码线程资源的状况下,最大量化业务代码。
更多精彩文章
- 分布式大并发系列
- 架构设计系列
- 趣学算法和数据结构系列
- 设计模式系列