1 背景
传统编程模型认为写一个变量就是直接写了相应的内存地址,但是在现代计算机架构中CPU写的是cache lines而不是直接写内存,这些缓存大部分是L1一级缓存,也就是说CPU的一个核写的内容另外的核看不见,为了把变化广播给别的核(这样才能同样广播到别的线程),需要把cache line的变化同步到其他核的cache。
JVM运行环境下,要做到上述的线程间共享,需要显式地给变量打上volatile 标记或者使用Atomic原子包装数据结构或者使用锁。否则,一般变量值的变化不保证能实时同步给其它线程可见。为什么不标记所有变量都是volatile的呢?因为在CPU的多个核之间同步cache lines是很昂贵的操作!会拖慢CPU内核速度并且导致cache coherence protocol (the protocol CPUs use to transfer cache lines between main memory and other CPUs) 瓶颈,最终明显的拖慢运行。
2 Actor简介
Actor模型首先是由Carl Hewitt在1973定义, 由Erlang OTP 推广,其消息传递更加符合面向对象的原始意图。Actor属于并发组件模型,通过组件方式定义并发编程范式的高级阶段,避免使用者直接接触多线程并发或线程池等基础概念。简单理解为 Actor模型=状态+行为+消息。
2.1 面向对象
Actor 模型本质上是一种计算模型,基本的计算单元称之为Actor ,换言之,在Actor 模型中,所有的计算都是在Actor 中执行的。在面向对象编程里面,一切都是对象,在Actor模型里面,一切都是Actor ,并且Actor 之间完全隔离的,不会共享任何变量。
2.2 无锁
在并发编程中常需要关注锁和内存原子性等一系列线程问题,而Actor模型内部的状态由它自己维护即它内部数据只能由它自己修改(通过消息传递来进行状态修改),所以使用Actors模型进行并发编程可以很好地避免这些问题。Actor内部是以单线程的模式来执行的,类似于redis,所以Actor完全可以实现分布式锁类似的应用。
2.3 异步
Actor中的异步是通过各自的MailBox来实现的,Actor间进行逻辑交互时只需要把消息传递到对应的MailBox里。
2.4 容错
对于传统的编程方式都是在将来可能出现异常的地方去捕获异常来保证系统的稳定性,即防御式编程。但对于防御式编程,防御的一方永远不能100%的防御住所有将来可能出现代码缺陷的地方。比如在java代码中很多地方充斥着判断变量是否为null,这些就属于防御式编码最典型的案例。但是Actor模型的程序并不进行防御式编程,而是遵循“任其崩溃”的哲学,让Actor的管理者们来处理这些崩溃问题。比如一个Actor崩溃之后,supervisor可以选择创建新的实例或者记录日志。每个Actor的崩溃或者异常信息都可以反馈到supervisor那里,这就保证了Actor系统在管理每个Actor实例的灵活性。
3 MailBox
每个actor都有且仅有一个mailbox,mailbox相当于一个小型的队列,一旦sender发送消息,就将该消息入队到mailbox中。入队的顺序按照消息发送的时间顺序。这样的设计解耦了actor之间的关系,每个actor都处理各自的mailbox�。虽然所有actor可以同时运行,但它们处理mailbox消息时只能当前消息处理完毕后才会处理下一个消息。
当一个actor接收到消息后,它能做如下三件事中的任意一件:
- 创建有限数量的新actors
- 发送有限数量的消息给其他参与者
- 指定下一条消息到来时的行为
劣势:
1、Actor模型需要一个缓冲队列来缓冲消息。
2、由于Actor是在进程内保存自己状态的,因此无法统一控制Actor的生命周期。在内存使用过量时不能像无状态服务通过淘汰机制进行内存控制。
3、Actor 系统需要关注mailBox的设计。
使用场景:
1、有资源竞争的功能都可以使用Actor模型。
2、需要做到水平扩容的服务。