为何现代系统须要新的编程模型Akka

Akka中最重要的即是actor模型。html

几十年前,Carl Hewitt提出了actor模型,以做为在高性能网络下并行处理的一种方式。可是在当时并无这样的环境,现在硬件和基础设施能力已经遇上并超越了Carl Hewitt当时的预料。因此,那些想构建高性能分布式系统的组织遇到的使用面向对象编程(OOP)模型没法彻底解决的挑战,如今可使用actor模型解决。编程

现在,actor模型不只被认为是一种高效的解决方案,并且也已经在世界上一些要求苛刻的应用中获得了验证。为了突出actor模型所能解决的问题,本主题主要讨论传统编程思想与现代多线程多CPU架构之间的不匹配:后端

  • 封装的挑战
  • 共享内存的错觉
  • 调用栈的错觉

封装的挑战

OOP的核心是封装。封装规定对象内的数据不能直接从外部访问,只能调用方法来进行修改。对象须要暴露出一些安全的操做,这些安全操做用来维护其内部数据的约束。缓存

例如,对有序二叉树的操做不得违反二叉树有序的约束。调用者但愿排序是完整的,当查询树中某个数据时,他们须要可以依赖这个约束。安全

当咱们分析OOP运行时行为时,咱们有时会绘制一个时序图,显示方法调用的交互。微信

image.png

不幸的是,上图并不能准确地表示执行期间实例的生命周期。实际上,全部这些调用发生在同一线程上。网络

image.png

当您尝试模拟多线程状况时,上面的这种表达方式就变得更加清晰了。由于咱们能够经过下图来表示两个线程访问同一个实例:多线程

image.png

两个线程进入同一个对象相同的方法,可是对象的封装模型并不能很好的表达这其中发生的事情。两个线程能够以任意方式交错,想象一下,若是是多线程,这个问题会更加严重。架构

解决此问题的经常使用方法是给这些方法加锁。虽然这确保了在任何给定时间最多只有一个线程将进入该方法,但这是一种很是昂贵的策略:并发

  • 锁严重限制了并发性,它们在现代CPU架构上很是昂贵,须要操做系统暂停线程而且在以后还要恢复它。
  • 调用者线程被阻塞后没法执行任何其余有意义的工做。即便在桌面应用程序中这也是不可接受的,咱们但愿即便在后台有耗时比较久的做业运行时,也要保持面向用户的应用程序部分可以响应用户的请求。在后端,阻塞是彻头彻尾的浪费。有人可能认为这能够经过启动新线程来补偿,但线程的代价也很是高昂。
  • 使用锁还可能致使死锁。

这些现实致使了一种尴尬的局面:

  • 若是没有足够的锁,对象的正常状态会被破坏
  • 若是使用不少锁,性能会受到影响而且很容易致使死锁。

此外,锁只能在本地很好地工做。在协调跨多台机器时,惟一的选择是分布式锁。不幸的是,分布式锁的效率比本地锁效率要差几个级别,而且在扩展时有更多的限制。分布式锁须要在多台计算机上经过网络进行屡次通讯往返,所以还存在延迟。

在面向对象语言中,咱们不多考虑线程的执行路径。咱们常常将系统设想为一个由对象组织成的网络,它们对方法调用做出反应,并修改其内部状态,而后经过方法调用相互通讯,从而驱动整个应用程序运行。

image.png

可是,在多线程分布式环境中,其实是线程经过方法调用“遍历”此对象网络。所以,真正推进应用程序运行的是线程:

image.png

总结:

  • 对象封装只能保证单线程访问时的对象内部状态的安全,多线程执行时几乎总会致使内部状态的损坏。
  • 虽然锁彷佛是支持多线程环境下的补救措施,但实际上它们效率低,而且容易致使死锁。
  • 锁更适合在本地工做。

共享内存的错觉

在80-90年代的编程概念模型中,本地变量是直接写入到内存中的(这和咱们理解的本地变量是存在寄存器中是不同的)。在现代架构上,CPU是写入到缓存行而不是直接写入内存的。这些高速缓存大多数都在CPU内核中,也就是说,一个内核的写入不会被另外一个内核看到。为了使内核中的本地更改对另外一个核心可见,须要将缓存行传送到另外一个核心中。

在JVM中,咱们必须使用volatile标记或使用Atomic包装类明确表示变量要跨线程进行内存共享。不然,咱们只能先加锁而后访问它们。为何咱们不将全部变量都标记为volatile?由于跨核心同步缓存行是一项很是昂贵的操做!这样作会阻止内核去执行额外的工做,并致使缓存一致性协议出现瓶颈。

即便对于了解这种状况的开发人员来讲,肯定哪些变量应该被标记为volatile,或者使用哪一种atomic结构也是一种艺术。

总结:

  • 没有真正的共享内存,CPU核心就像网络上的计算机同样须要将数据块(高速缓存行)同步给其余CPU核心。CPU间通讯和网络通讯是类似的。
  • 经过将变量标记为volatile或使用Atomic结构来使CPU核心之间的数据进行同步是能够被替代的,咱们可使用一种更有纪律和原则性的方式,将本地变量保存到并发实体内,而后经过消息显式地在并发实体之间传播数据或事件。

调用栈的错觉

今天,咱们将调用栈视为理所固然。可是,它们是在一个并发编程并不重要的时代发明的,由于那时多CPU系统并不常见,调用栈不会跨线程。

当主线程打算将任务委托给“后台”时,这实际上就是将任务委托给另一个工做线程,实际上就是主线程将一个任务对象放入工做线程中的一个共享队列里,工做线程负责从这个队列里获取任务来执行,这就容许主线程继续前进并执行其余任务。

他的第一个问题是,工做线程如何通知主线程任务已完成?当任务因异常而失败时会出现更严重的问题,异常传播到哪里?真实状况是它将传播到工做线程的异常处理程序,而后彻底忽略了实际的“调用者”是主线程:

image.png

这是一个严重的问题。主线程的调用栈上不能捕获这个异常,工做线程如何处理这种状况?须要以某种方式通知主线程,例如将异常放在主线程预先准备存放结果的地方,但若是主线程一直没有收到通知,任务也即丢失!

当工做线程在执行任务时出现BUG,致使工做线程关闭,这时谁来从新启动一个线程来处理这个任务,而且该任务如何恢复到正常的状态。这都是问题。

总结:

  • 为了在当前系统上实现有意义的高性能并发,线程必须以有效的方式在彼此之间委派任务而且不会发生阻塞。使用这种任务委托方式,基于调用栈的错误处理会中断,而且须要引入新的、明确的错误通知机制。失败处理成为并发系统中要考虑的一部分。
  • 并发系统须要处理服务故障并须要具备从中恢复的方法。此类服务的客户端须要知道任务/消息可能在从新启动期间丢失。即便没有发生丢失,因为先前排队的任务比较多,垃圾收集形成的延迟等,响应可能会被延迟。面对这些,并发系统应该以超时的形式处理响应,就像网络/分布式系统同样。

原文为akka官网连接:doc.akka.io/docs/akka/c…

接下来的文章,让咱们看看actor模型如何克服这些挑战。

若是以为这篇文章能让你学到知识,可否帮忙转发,将知识分享出去。 若是想第一时间学习更多的精彩的内容,请关注微信公众号:1点25

相关文章
相关标签/搜索