这段时间因为忙毕业前先后后的事情,拖更了好久,表示很是抱歉,回归后的第一篇文章主要是看到了Akka最新文档中写的What problems does the actor model solve?,阅读完后以为仍是蛮不错,能简洁清晰的阐述目前并发领域遇到的问题,并为什么利用Actor模型能够解决这些问题,本文主要是利用本身的理解将这篇文章进行翻译,有不足之处还请指出。html
Akka使用Actor模型来克服传统面向对象编程模型的局限性,并应对高并发分布式系统所带来的挑战。 充分理解Actor模型是必需的,它有助于咱们认识到传统的编程方法在并发和分布式计算的领域上的不足之处。编程
面向对象编程(OOP)是一种普遍采用的,熟悉的编程模型,它的一个核心理念就是封装,并规定对象封装的内部数据不能从外部直接访问,只容许相关的属性方法进行数据操做,好比咱们熟悉的Javabean中的getX,setX等方法,对象为封装的内部数据提供安全的数据操做。缓存
举个例子,有序二叉树必须保证树节点数据的分布规则,若你想利用有序二叉树进行查询相关数据,就必需要依赖这个约束。安全
当咱们在分析面向对象编程在运行时的行为时,咱们可能会绘制一个消息序列图,用来显示方法调用时的交互,以下图所示:网络
但上述图表并不能准确地表示实例在执行过程当中的生命线。实际上,一个线程执行全部这些调用,而且变量的操做也在调用该方法的同一线程上。为刚才的序列图加上执行线程,看起来像这样:数据结构
但当在面对多线程的状况下,会发现此前的图愈来愈混乱和变得不清晰,如今咱们模拟多个线程访问同一个示例:多线程
在上面的这种状况中,两个线程调用同一个方法,但别调用的对象并不能保证其封装的数据发生了什么,两个调用的方法指令能够任意方式的交织,没法保证共享变量的一致性,如今,想象一下在更多线程下这个问题会更加严重。架构
解决这个问题最一般的方法就是在该方法上加锁。经过加锁能够保证同一时刻只有一个线程能进入该方法,但这是一个代价很是昂贵的方法:并发
锁很是严重的限制并发,它在如今的CPU架构上代价是很是大的,它须要操做系统暂停和重启线程。异步
调用者的线程会被阻塞,以至于它不能去作其余有意义的任务,举个例子咱们但愿桌面程序在后台运行的时候,操做UI界面也能获得响应。在后台,,线程阻塞彻底是浪费的,有人可能会说能够经过启动新线程进行补偿,但线程也是一种很是昂贵的资源。
使用锁会致使一个新的问题:死锁。
这些现实存在的问题让咱们只能二者选一:
不使用锁,但会致使状态混乱。
使用大量的锁,可是会下降性能并很容易致使死锁。
另外,锁只能在本地更好的利用,当咱们的程序部署在不一样的机器上时,咱们只能选择使用分布式锁,但不幸的是,分布式锁的效率可能比本地锁低好几个量级,对后续的扩展也会有很大的限制,分布式锁的协议要求多台机器在网络上进行相互通讯,所以延迟可能会变得很是高。
在面向对象语言中,咱们不多会去考虑线程或者它的执行路径,咱们一般将系统想象成许多实例对象链接成的网络,经过方法调用,修改实例对象内部的状态,而后经过实例对象以前的方法调用驱动整个程序进行交互:
而后,在多线程分布式环境中,实际上线程是经过方法调用遍历这个对象实例网络。所以,线程是方法调用驱动执行的:
总结:
对象只能保证在单一线程中封装数据的正确性,在多线程环境下可能会致使状态混乱,在同一个代码段,两个竞争的线程可能致使变量的不一致。
使用锁看起来能够在多线程环境下保证封装数据的正确性,但实际上它在程序真是运行时是低效的而且很容易致使死锁。
锁在单机工做可能还不错,可是在分布式的环境表现的很不理想,扩展性不好。
在80-90年代的编程模型概念中,写一个变量至关于直接把它写入内存,可是在现代的计算机架构中,咱们作了一些改变,写入相应的缓存中而不是直接写入内存,大多数缓存都是CPU核心的本地缓存,可是由一个CPU写入的缓存对其余CPU是不可见的。为了让本地缓存的变化对其余CPU或者线程可见的话,缓存必须进行交互。
在JVM上,咱们必须使用volatile标识或者Atomic包装类来保证内存对跨线程的共享,不然,咱们只能用锁来保证共享内存的正确性。那么咱们为何不在全部的变量上都加volatile标识呢?由于在缓存间交互信息是一个代价很是昂贵的操做,并且这个操做会隐式的阻止CPU核心不能去作其余的工做,而且会致使缓存一致性协议(缓存一致性协议是指CPU用于在主内存和其余CPU之间传输缓存)的瓶颈。
即便开发者认识到这些问题,弄清楚哪些内存位置须要使用volatile标识或者Atomic包装类,但这并不是是一种很好的解决方案,可能到程序后期,你都不清楚本身作了什么。
总结:
没有真正的共享内存了,CPU核心就像网络上的计算机同样,将数据块(高速缓存行)明确地传递给彼此。CPU间的通讯和网络通讯有更多的共同点。 如今经过CPU或网络计算机传递消息是标准的。
使用共享内存标识或者Atomic数据结构来代替隐藏消息传递,其实有一种更加规范的方法就是将共享状态保存在并发实体内,并明确并发实体间经过消息来传递事件和数据。
今天,咱们还常常调用堆栈来进行任务执行,可是它是在并发并不那么重要的时代发明的,由于当时多核的CPU系统并不常见。调用堆栈不能跨线程,因此不能进行异步调用。
线程在将任务委托后台执行会出现一个问题,实际中,是将任务委托给另外一个线程执行,这不是简单的方法调用,而是有本地的线程直接调用执行,一般来讲,一个调用者线程将任务添加到一个内存位置中,具体的工做线程能够不断的从中选取任务进行执行,这样的话,调用者线程没必要阻塞能够去作一些其余的任务了。
可是这里有几个问题,第一个就是调用者如何受到任务完成的通知?还有一个更重要的问题是当任务发生异常出现错误后,异常会被谁处理?异常将会被具体执行任务的工做线程所处理并不会关心是哪一个调用者调用的任务:
这是一个很严重的问题,具体执行任务的线程是怎么处理这种情况的?具体执行任务去处理这个问题并非一个好的方案,由于它并不清楚该任务执行的真正目的,并且调用者应该被通知发生了什么,可是实际上并无这样的结构去解决这个问题。假如并不能正确的通知,调用者线程将不会的到任何错误的信息甚至任务都会丢失。这就比如在网络上你的请求失败或者消息丢失却得不到任何的通知。
在某些状况,这个问题可能会变得更糟糕,工做线程发生了错误可是其自身却没法恢复。好比一个由bug引发的内部错误致使了线程的关闭,那么会致使一个问题,到底应该由谁来重启线程而且保存线程以前的状态呢?表面上看,这个问题是能够解决的,但又会有一个新的意外可能发生,当工做线程正在执行任务的时候,它便不能共享任务队列,而事实上,当一个异常发生后,并逐级上传,最终可能致使整个任务队列的状态所有丢失。因此说即便咱们在本地交互也可能存在消息丢失的状况。
总结:
实现任何一个高并发且高效性能的系统,线程必须将任务有效率的委托给别的线程执行以致不会阻塞,这种任务委托的并发方式在分布式的环境也适用,可是须要引入错误处理和失败通知等机制。失败成为这种领域模型的一部分。
并发系统适用任务委托机制须要去处理服务故障也就意味须要在发生故障后去恢复服务,但实际状况下,重启服务可能会丢失消息,即便没有发生这种状况,调用者获得的回应也可能由于队列等待,垃圾回收等影响而延迟,因此,在真正的环境中,咱们须要设置请求回复的超时时间,就像在网络系统亦或者分布式系统。
综上所述,一般的编程模型并不适用现代的高并发分布式系统,幸运的是,咱们能够没必要抛弃咱们了解的知识,另外,Actor用很好的方式帮咱们克服了这些问题,它让咱们以一种更好的模型去实现咱们的系统。
咱们重点需求的是如下几个方面:
使用封装,可是不使用锁。
构建一种实体可以处理消息,更改状态,发送消息用来推进整个程序运行。
没必要担忧程序执行与真实环境的不匹配。
Actor模型能帮咱们实现这些目标,如下是具体描述。
不一样于方法调用,Actor模型使用消息进行交互。发送消息的方式不会将发送消息方的执行线程转换为具体的任务执行线程。Actor能够不断的发送和接收消息但不会阻塞。所以它能够作更多的工做,好比发送消息和接收消息。
在面对对象编程上,直到一个方法返回后,才会释放对调用者线程的控制。在这这一方面上,Actor模型跟面对对象模型相似,它对消息作出处理,并在消息处理完成后返回执行。咱们能够模拟这种执行模式:
可是这种方式与方法调用方式最大的区别就是没有返回值。经过发送消息,Actor将任务委托给另外一Actor执行。就想咱们以前说的堆栈调用同样,加入你须要一个返回值,那么发送Actor须要阻塞或者与具体执行任务的Actor在同一个线程中。另外,接收Actor以消息的方式返回结果。
第二个关键的变化是继续保持封装。Actor对消息处理就就跟调用方法同样,可是不一样的是,Actor在多线程的状况下能保证自身内部的状态和变量不会被破坏,Actor的执行独立于发送消息的Actor,而且同一个Actor在同一个时刻只处理一个消息。每一个Actor有序的处理接收的消息,因此一个Actor系统中多个Actor能够并发的处理本身的消息,充分的利用多核CPU。由于一个Actor同一时刻最多处理一个消息,因此它不须要同步机制保障变量的一致性。因此说它并不须要锁:
总而言之,Actor执行的时候会发生如下行为:
1.Actor将消息加入到消息队列的尾部。
2.假如一个Actor并未被调度执行,则将其标记为可执行。
3.一个(对外部不可见)调度器对Actor的执行进行调度。
4.Actor从消息队列头部选择一个消息进行处理。
5.Actor在处理过程当中修改自身的状态,并发送消息给其余的Actor。
6.Actor
为了实现这些行为,Actor必须有如下特性:
消息会被添加到Actor的信箱中,Actor的行为能够当作Actor是如何对消息作出回应的(好比发送更多消息或者修改自身状态)。执行环境提供一组线程池,用于执行Actor的这些行为操做。
Actor是一个很是简单的模型并且它能够解决先前提到的问题:
继续使用封装,但经过信号机制保障不需传递执行(方法调用须要传递执行线程,但发送消息不须要)。
不须要任何的锁,修改Actor内部的状态只能经过消息,Actor是串行处理消息,能够保障内部状态和变量的正确性。
由于不会再任何地方使用锁,因此发送者不会被阻塞,成千上万个Actor能够被合理的分配在几十个线程上执行,充分利用了现代CPU的潜力。任务委托这个模式在Actor上很是适用。
Actor的状态是本地的,不可共享的,变化和数据只能经过消息传递。
Actor再也不使用共享的堆栈调用,因此它要以不一样的方式去处理错误。这里有两种错误须要考虑:
第一种状况是当任务委托后再目标Actor上因为任务自己错误而失败了(典型的如验证错误,好比不存在的用户ID)。在这个状况下,Actor服务自己是正确的,只是相应的任务出错了。服务Actor应该想发送Actor发送消息,已告知错误状况。这里没什么特殊的,错误做为Actor模型的一部分,也能够当作消息。
第二种状况是当服务自己遇到内部故障时。Akka强制全部Actor被组织成一个树状的层次结构,即建立另外一个Actor的Actor成为该新Actor的分级。 这与操做系统将流程组合到树中很是类似。就像进程同样,当一个Actor失败时,它的父actor被通知,并对失败作出反应。此外,若是父actor中止,其全部子Actor也被递归中止。这中形式被称为监督,它是Akka的核心:
监管者能够根据被监管者(子Actor)的失败的错误类型来执行不一样的策略,好比重启该Actor或者中止该Actor让其它Actor代替执行任务。一个Actor不会平白无故的死亡(除非出现死循环之类的状况),而是失败,并能够将失败传递给它的监管者让其作出相应的故障处理策略,固然也可能会被中止(若被中止,也会接收到相应的消息指令)。一个Actor总有监管者就是它的父级Actor。Actor从新启动是不可见的,协做Actor能够帮其代发消息直到目标Actor重启成功。