Akka入门(一)Akka简介与为什么使用Akka

 AKKA是什么

AKKA是一套开源库,用于设计跨处理器和跨网络的可扩展弹性系统Akka允许开发者专注于满足业务需求,而不是编写低级代码以提供可靠的行为,容错和高性能。

分布式系统环境中,组件崩溃而不响应,消息丢失而没有线路上的跟踪,并且网络延迟会发生波动。这些问题经常发生在精心管 理的数据中心内部环境中 - 在虚拟化体系结构中更是如此。

为了帮助您处理这些现实问题,Akka提供:

  • 多线程行为,不使用原子或锁等低级并发结构 - 使您甚至不必考虑内存可见性问题。
  • 系统及其组件之间的透明远程通信 - 使您免于编写和维护困难的网络代码。
  • 集群,高可用性架构,具有弹性,可按需扩展或扩展 - 使您能够提供真正的响应式系统。

为什么使用Akka(为什么现代系统需要一个新的编程模型)

(1)封装的挑战

OOP的核心支柱是封装。封装规定对象的内部数据不能直接从外部访问; 它只能通过调用一组策划方法来修改。该对象负责公开保护其封装数据的不变性的安全操作。

当我们分析OOP运行时行为时,我们有时会绘制一个消息序列图,显示方法调用的交互。例如:

不幸的是,上图不能准确地表示执行期间实例的生命线。实际上,线程执行所有这些调用,并且不变量的强制发生在调用该方法的同一线程上。使用执行线程更新图表,它看起来像这样:

 

但当多线程的情况下,我们整齐地绘制的图表变得不充分。有的时候会有多个线程访问同一个实例。

 

有一个执行部分,其中两个线程进入相同的方法。不幸的是,对象的封装模型并不保证该部分中发生的事情。两个调用的指令可以以任意方式交错。这种情况会在多线程的情况下更加复杂。

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

  • 锁严重限制了并发性,它们在现代CPU架构上非常昂贵,需要从操作系统中重载以暂停线程并在以后恢复它。
  • 调用者线程现在被阻止,因此它无法执行任何其他有意义的工作。即使在桌面应用程序中这也是不可接受的,我们希望即使在长后台作业运行时,也要保持面向用户的应用程序部分(其UI)能够响应。在后端,阻塞是彻头彻尾的浪费。有人可能认为这可以通过启动新线程来补偿,但线程也是一种代价高昂的抽象。
  • Locks引入了一个新的威胁:死锁。 

这些现实导致了一个局面:

 

此外,锁只能在本地很好地工作。在协调跨多台机器时,唯一的选择是分布式锁。不幸的是,分布式锁定的效率比本地锁定效率低几个级别,并且通常会在扩展时施加限制。分布式锁定协议需要在多台计算机上通过网络进行多次通信往返,因此造成大量延迟。

在面向对象语言中,我们很少考虑线程或线性执行路径。我们经常将系统设想为对象实例的网络,它们对方法调用作出反应,修改其内部状态,然后通过驱动整个应用程序状态的方法调用相互通信:

但是,在多线程分布式环境中,实际发生的是线程通过以下方法调用“遍历”此对象实例网络。

 

 

  • 没有足够的锁,状态就会被破坏。
  • 由于存在许多锁定,性能会受到影响并且很容易导致死锁。

总结

 

 

(2) 现代计算机体系结构中共享内存的错觉

  • 对象只能保证面对单线程访问时的封装(保护不变量),多线程执行几乎总会导致内部状态损坏。
  • 虽然锁似乎是支持多线程封装的自然补救措施,但实际上它们效率低,并且在任何实际规模的应用中都容易导致死锁。
  • 锁在本地工作,尝试使它们分布存在,但提供扩展潜力十分有限。

在现代架构上 - 如果我们稍微简化一下 - CPU正在写入缓存,而不是直接写入内存。这些高速缓存中的大多数都是CPU内核的本地高速缓存,也就是说,一个内核的写入不会被另一个内核看到。为了使本地更改对另一个核心可见,从而对另一个线程可见,需要将缓存行传送到另一个核心的缓存。

在JVM上,我们必须使用volatile标记或Atomic包装器明确表示要跨线程共享的内存位置。否则,我们只能在锁定的部分访问它们。为什么我们不将所有变量标记为volatile?因为跨核心运送缓存行是一项非常昂贵的操作!这样做会隐含地使所涉及的核心停止执行额外的工作,并导致高速缓存一致性协议(协议CPU用于在主存储器和其他CPU之间传输高速缓存线)的瓶颈。结果是放缓的幅度。

总结

 

 

(3)调用堆栈的错觉

当线程打算将任务委托给“后台”时会出现问题。实际上,这实际上意味着委托给另一个线程。通常发生的是,“调用者”将一个对象放入由工作线程(“被调用者”)共享的内存位置,然后工作线程在某个事件循环中将其拾取。这允许“调用者”线程继续前进并执行其他任务。

问题是:如何通知“调用者”完成任务?但是当任务因异常而失败时会出现更严重的问题。异常传播到哪里?它将传播到工作线程的异常处理程序,完全忽略了实际的“调用者”是谁:

 

 

  • 没有真正的共享内存,CPU核心就像网络上的计算机一样明确地将数据块(高速缓存行)传递给彼此。CPU间通信和网络通信比许多人意识到的更为共同。现在可以通过CPU或联网计算机传递消息。
  • 不是通过标记为共享或使用原子数据结构的变量隐藏消息传递方面,而是更有纪律和原则性的方法是将状态本地保持为并发实体,并通过消息显式地在并发实体之间传播数据或事件。

这是一个严重的问题。工作线程如何处理这种情况?它可能无法解决问题,因为它通常无视任务失败的目的。需要以某种方式通知“调用者”线程,但没有调用堆栈可以解除异常。失败通知只能通过旁道进行,例如将错误代码置于“调用者”线程否则预期结果准备好的位置。如果没有这个通知,“调用者”永远不会收到失败通知,任务就会丢失!这与网络系统如何在没有任何通知的情况下丢失/失败消息/请求的情况类似。

当事情出现问题时,这种糟糕的情况会变得更糟,并且由线程支持的工作人员遇到错误并最终处于无法恢复的状态。例如,由bug引起的内部异常会冒泡到线程的根目录并使线程关闭。这立即引发了一个问题,谁应该重新启动线程托管的服务的正常操作,以及如何将其恢复到已知良好的状态?乍一看,这似乎是可以管理的,但我们突然面临一个新的意外现象:线程当前正在处理的实际任务不再位于从中获取任务的共享内存位置(通常是队列) )。事实上,由于异常到达顶部,展开所有调用堆栈,任务状态完全丢失!我们已经丢失了一条消息,即使这是本地通信而没有涉及网络(消息丢失是预期的)。

综上所述:

 

本文翻译自akka官方文档,并添加了部分个人理解如有需要请参考  https://akka.io/docs/

 

 

  • 为了在当前系统上实现任何有意义的并发性和性能,线程必须以有效的方式在彼此之间委派任务而不会阻塞。使用这种任务委托并发方式(对于网络/分布式计算更是如此),基于调用堆栈的错误处理会中断,并且需要引入新的,明确的错误信令机制。失败成为域模型的一部分。
  • 具有工作委托的并发系统需要处理服务故障并具有从中恢复的原则性方法。此类服务的客户端需要知道任务/消息可能在重新启动期间丢失。即使没有发生丢失,由于先前排队的任务(长队列),垃圾收集造成的延迟等,响应可能会被任意延迟。面对这些,并发系统应该以超时的形式处理响应期限 ,像网络/分布式系统。 

你可能感兴趣的:(Akka)