官方链接
本文从传统面向对象的封装,内存共享以及并发错误处理等几个方面对比了传统并发模型的缺陷。
通过对比actor模型和传统模型,更好的理解actor模型在高并发的分布式场景中的优势.
Actor模型在十多年前被Carl Hewitt提出用来在高性能网络环境中处理并发问题--在当时的环境没有价值.如今硬件和基础设施能力赶上了甚至超过了Carl Hewitt的预见.因此当组织要构建苛刻需求(demanding requirements)的分布式系统遇到了到了挑战,我们不能够完全通过传统的面向对象模型解决这些问题,但是使用actor模型却能够从中受益.
如今,actor模型不仅被认识为是一个高效的解决方案-他也在很多世界上需求苛刻的应用中被证明.
为了突出显示actor模型解决的问题,下面将讨论现代多线程多CPU与传统编程的观点不匹配的地方.
面向对象的一个核心概念就是 封装(encapsulation) .封装规定内部数据不能够直接的被外部访问.只能够通过暴漏的方法访问.对象需要负责暴露安全的操作来保证他的数据的不变性质.
举个例子在一个有序二叉树实现上的操作,不能够允许违反树排序不变的. 调用方期望树是完好的, 并且在查询树的某一数据段时,必须依赖这个约束.
当我们分析面向对象的实时行为,我们可以通过时序图来展示方法调用.
不幸的是,上面的图不能够确切的代表实例在执行时的生命线.实际中,一个线程执行这些调用,当不变量的执行发生在调用的同一个线程.更新上面的图
当我们试着模拟多线程中发生的事情,使用这种方式会变得清晰.突然我们整齐的图变得零乱.我们可以试着说明多线程是如何访问同一个实例:
上图表明两个线程访问同一个方法.不幸的是,封装对象的模型不能够保证在这个情况下发生什么. 两个方法调用可以以任意方式进入,所以在没有特定的协助下我们不能保证不变量不变了.
通常解决这个问题的方法是添加一个锁.这可以保证同一时刻只有一个线程进入这个方法,但是代价非常昂贵:
这些问题会导致一个必败的情况:
额外的,锁只能在本地工作,当我们需要多台机器协作时,唯一的解法是分布式锁.分布式锁性能比本地锁差了很多而且对扩展限制比较严格. 分布式锁协议需要多台机器多次得网络往返通信,所以延迟会飞涨(latency goes through the roof)
在面向对象的语言我们很少考虑线程或者线性执行路径.我们通常把一个系统想象成一个对象实例的网络,对方法调用做出反应,修改他们内部状态,然后互相通信驱动着整个应用状态向前.
下图是对象通过方法相互调用
然而在多线程的分布式环境中,实际发生的是线程通过方法调用"穿过了"对象实例的网络.因此,线程才是真正
的驱动执行的东西
下图三个颜色 三个线程中方法互相调用.
总结:
80-90年代的概念写入变量意味着直接写到内存中(局部变量也许之存在寄存器中).现代的架构.CPUs 将变量写在cache lines中. 大部分的缓存都是在CPU 核心中,多以会出现一个核心对其他的不可见.为了使局部改变对于其他核心可见,从而让别的线程可见,缓存行需要将改变传到别的核心缓存中.
在JVM,我们不得不通过使用 volatile关键字或者 原子的修饰来明确标记存储是要跨线程共享的.因此,我们只能通过锁定区域来访问他们。因为calche lines 跨核心是非常昂贵的操作,所以我们不能够把所有的变量都标记为volatile. 如果这么做将隐式的暂停了相关参与核心做额外的工作,并且造成缓存一致性协议(MESI)的瓶颈(cpus 通常使用这个协议在主存和其他cpu之间传输cacle lines).最终导致效率数量级的下降.
总结:
如今,我们经常把调用堆栈当作理所当然.但是,他们是在多CPU系统不流行的时代被发明的.调用堆栈不能跨线程所以不能模拟异步调用链.
这个问题在一个线程打算委托一个任务给"backgroud"出现.实际中,这意味着委托给其他线程.这种方式不是一个简单得方法/函数调用,因为这种调用是严格的在线程本地.通常发生,"调用者"把一个对象放在一个和工作线程("callee")共享得内存位置, 而调用者又在某些事件循环中获取它.这允许"caller"线程继续或者去做别的任务.
第一个问题是,怎么样通知"caller"任务已经完成了? 与之而来的一个更严重的问题出现,当一个任务以异常状态失败了.怎么把异常传播过去?它将传播给工作线程的异常处理器完全的忽略谁是实际的"caller":
这是个严肃的问题.工作线程怎么处理这个问题呢? 它似乎不能解决这个问题因为它通常不知道失败任务的目的.调用线程需要以某种方式被通知,但是没有调用堆栈去释放异常.失败的通知只能够被别的通道完成,比如把一个错误码放到调用者线程期望获取结果的位置。如果这个通知没有到位,调用者永远不能够获得失败的通知,这个任务就丢失了! 这个和网络系统工作的很相似,消息/请求可能丢失/失败 而没有任何通知.
更坏的情况是当真的出现了错误并且一个工作线程遇到了bug或者一个不可恢复的情况。比如,一个内部一场造成了一个bug冒出来到线程的root 并且使线程关闭了. 这时谁应该重启这个线程承载的服务的正常运行,并且怎么重置成一个好的状态? 乍一看,貌似可以被管控,但我们忽然要面对一个新的,不期望的现象(phenomenon):当前线程正在工作的实际任务,已经不在共享内存的位置-任务获取的地方(通常是队列). 事实上,由于异常到达了最上级,释放了所有的堆栈,这个任务状态已经完全丢失了! 我们甚至没有网络的参与就在本地通信中丢失了消息.
总结: