Akka简介多线程问题《four》译

原文:https://doc.akka.io/docs/akka/current/guide/introduction.html?language=scalahtml

    Akka是一套开放源码库,用于设计可伸缩的、具备弹性的系统,跨越处理器内核和网络。Akka容许您专一于知足业务需求,而不是编写低级别代码来提供可靠的行为、容错和高性能。编程

    许多常见作法和使用的编程模型并不能解决和设计现代计算机体系结构所固有的重要挑战。为了解决那些问题,分布式系统必须应对组件崩溃不能响应,消息丢失时没有轨迹,网络延迟波动。这些问题常常发生在精心管理的数据中心环境中-甚至在虚拟化架构中更是如此。后端

    为了帮助你处理这些现实,Akka提供了:缓存

  • 多线程行为,而不使用低级别的并发结构,如原子或锁-减轻了您甚至不考虑内存可见性的问题。
  • 系统及其组件之间的透明远程通讯-使您没必要编写和维护困难的网络代码。
  • 一种集群的、高可用性的体系结构,它是弹性的,能够按需向内或向外扩展,使您可以交付一个真正的响应式系统。

    Akka对Actor模型的使用提供了一个抽象级别,使得编写正确的并发、并行和分布式系统变得更容易。角色模型跨越了所有Akka库,为您提供了一种理解和使用它们的一致方式。所以,Akka提供了深度的集成,您没法经过挑选库来解决单个问题并尝试将它们组合在一块儿来实现。安全

    经过学习Akka和如何使用Actor模型,您将得到一组普遍而深刻的工具,这些工具能够在统一的编程模型中解决困难的分布式/并行系统问题,在这个模型中,一切都紧密而有效地结合在一块儿。网络

How to get started

    若是这是您第一次体验Akka,咱们建议您从运行一个简单的HelloWorld项目开始。有关下载和运行HelloWorld示例的说明,请参阅QuickStart指南。QuickStart指南向您介绍了如何定义角色系统、角色和消息以及如何使用测试模块和日志记录的示例代码。在30分钟内,您应该可以运行HelloWorld示例并了解它是如何构造的。数据结构

    本入门指南提供了下一级别的信息。它涵盖了为何角色模型适合现代分布式系统的须要,并包括一个教程,将帮助您进一步了解Akka。主题包括:多线程

Why modern systems need a new programming model

    角色模型是几十年前由卡尔·休伊特(CarlHewitt)提出的,做为在高性能网络中处理并行处理的一种方式-这种环境在当时是不可用的。今天,硬件和基础设施能力已经遇上并超过了休伊特的设想。所以,构建具备严格需求的分布式系统的组织遇到了传统的面向对象编程(OOP)模型没法彻底解决的挑战,但它能够从角色模型中受益。架构

    今天,角色模型不只被认为是一种很是有效的解决方案-它已经在世界上一些最苛刻的应用程序的生产中获得了证实。为了突出角色模型解决的问题,本主题讨论传统编程与现代多线程多CPU体系结构的现实之间的不匹配:并发

The challenge of encapsulation

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

    例如,对有序二叉树实现的操做必须不容许违反树的顺序不变。调用方但愿顺序是完整的,而且当查询树以获取特定的数据时,他们须要可以依赖于这个约束。

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

    不幸的是,上面的图表不能准确地表示执行过程当中实例的生命线。实际上,一个线程执行全部这些调用,执行发生在调用方法的在同一个线程上。用执行线程更新关系图,以下所示:

    当您尝试对多线程所发生的状况进行建模时,这种澄清的意义就变得清楚了。忽然间,咱们画得很整齐的图表变得不够用了。咱们能够尝试说明访问同一个实例的多个线程:

    有一段执行,两个线程进入相同的方法。不幸的是,对象的封装模型并不能保证该部分中发生了什么。这两个调用的指令能够任意的方式交织在一块儿,这就消除了保持变量不变的任何但愿,而无需在两个线程之间进行某种类型的协调。如今,想象一下这个问题因为存在许多线程而变得更加复杂。

    解决此问题的常见方法是围绕这些方法添加一个锁。虽然这能够确保在任何给定的时间最多有一个线程进入该方法,但这是一种代价很高的策略:

  • 锁严重限制了并发性,在现代的cpu体系结构上,它们的代价很是高昂,须要从操做系统中重装线程以挂起线程并在之后恢复它。
  • 调用者线程如今被阻塞,所以它不能执行任何其余有意义的工做。即便在桌面应用程序中,这也是不可接受的,即便在运行长时间的后台做业时,咱们也但愿保持应用程序面向用户的部分(其用户界面)具备响应性。在后端,阻塞是彻底浪费的。人们可能认为启动新线程能够弥补这一点,但线程也是一种代价高昂的抽象。
  • 锁引入了一个新的威胁:死锁。

    这些现实致使了一种不成功的局面:

  • 若是没有足够的锁,状态就会损坏。
  • 有了许多锁,性能就会受到影响,很容易致使死锁。

    此外,锁只能在本地正常工做。当涉及到跨多台计算机进行协调时,惟一的选择是分布式锁。不幸的是,分布式锁的效率比本地锁低几个数量级,而且一般对扩展施加一个硬限制。分布式锁协议须要在多台计算机上经过网络进行屡次通讯,所以延迟时间太高。

    在面向对象语言中,咱们一般不多考虑线程或线性执行路径。咱们常常设想一个系统是一个对象实例网络,它对方法调用做出反应,修改它们的内部状态,而后经过方法调用相互通讯,从而推进整个应用程序状态向前:

    然而,在多线程分布式环境中,实际发生的事情是线程经过如下方法调用“遍历”这个对象实例网络。所以,真正驱动线程执行的是:

    总的来讲:

  • 对象只能保证封装(保护不变量)在面对单线程访问时,多线程执行几乎老是致使损坏的内部状态。在相同的代码段中有两个争用线程可能会破坏每一个不变量。
  • 虽然锁彷佛是支持多线程封装的天然补救措施,但实际上它们效率低下,在任何实际规模的应用程序中都很容易致使死锁。
  • 锁在本地工做,试图使它们分布在本地,但扩展的可能性有限。

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

    80‘-90年代的编程模型认为写入变量意味着直接写入内存位置(这可能模糊了局部变量可能只存在于寄存器中)。对于现代架构,若是咱们简化了一些事情,那么cpu就会编写缓存行而不是直接写内存。大多数缓存都是本地cpu核心,也就是说,一个核心写的都不能被另外一个内核所显示。为了使本地更改可见于另外一个核心,所以对于另外一个线程来讲,缓存行须要被发送到其余核心缓存。

    在jvm上,咱们必须显式地表示经过使用volatile标记或原子包装来跨线程共享的内存位置。不然,咱们只能在锁定的部分访问它们。为何咱们不把全部变量标记为volatile?由于传送高速缓存线跨越核心是一个很是昂贵的操做!这样作会隐式地将涉及额外工做所涉及的核心拖放,从而致使缓存一致性协议(协议cpu用于传输主内存和其余cpu之间缓存线)瓶颈。结果是减速幅度。

    即便对于了解这种状况的开发人员来讲,要肯定哪些内存位置应该标记为volatile,或者使用哪些原子结构是一种黑暗艺术。

总的来讲:

  • 再也不存在真正的共享内存,cpu内核就像网络上的计算机同样,经过显式地传递块数据(缓存行)。cpu之间的通讯和网络通讯比许多实现都更加常见。经过消息传递是当前的标准,它跨越cpu或联网计算机。
  • 经过标记为共享或使用原子数据结构的变量来隐藏消息传递方面,更有纪律和有原则的方法是将状态保持到并发实体中,并经过消息显式地传播并发实体之间的数据或事件。

call stacks的错觉

    今天,咱们常常用call stacks来作理所固然的事。可是,它们是在一个并行编程并不重要,多cpu系统并不常见的时代发明的。call stacks不会跨线程,不模拟异步调用链。

    当线程打算将任务委托到“后台”时,会出现问题。实际上,这实际上意味着委托另外一个线程。这不能是简单的方法/函数调用,由于调用对线程是严格的本地调用。一般发生的是,调用者将对象放在一个由工做线程共享的内存位置(“被调用者”),反过来,它会在某些事件循环中从新选择它。这样可让调用方线程继续运行并执行其余任务。

    第一个问题是,如何才能通知“caller”完成任务?可是当任务与异常失败时会出现更严重的问题。异常传播到哪里?它可能将传播到工做线程的异常处理程序,彻底忽略实际的调用方是谁:

    这是一个严重的问题,工做线程如何处理这种状况?它极可能没法解决这个问题,由于它一般忽略了失败任务的目的。须要通知“调用方”线程,可是没有一个调用堆栈以异常解除。故障通知只能经过侧通道来完成,例如,在调用“调用方”线程时,将错误代码放在另外一个位置上,不然会预期结果一旦就绪。若是此通知未到位,“调用方”将永远不会通知失败,任务丢失!这与网络系统工做相似,由于在没有任何通知的状况下,消息/请求能够丢失/失败。

    当事情发生错误时,这种坏状况会变得更糟,而一个由线程支持的遇到了一个bug,最终会出现没法恢复的情况。例如,由bug引起的内部异常,将其抛到root线程,并使线程关闭。这当即引起了一个问题:谁应该从新启动线程托管服务的正常操做,以及如何恢复到一个已知的状态?乍一看,这彷佛是能够控制的,可是咱们忽然面临一个新的、意想不到的现象:线程当前正在运行的实际任务再也不位于任务从(一般是队列)中的共享内存位置。事实上,因为异常到达顶部,解除全部调用堆栈,任务状态彻底丢失!尽管这是本地通讯,但没有联网(预计消息损失将被预期),咱们已经失去了一个消息。

总结:

  • 为了在当前系统上实现任何有意义的并发性和性能,线程必须在不阻塞的状况下高效地相互委托任务。对于这种类型的任务委托并发(在网络/分布式计算中更是如此),基于堆栈的调用错误处理中断,须要引入新的显式错误信号机制。失败成为域模型的一部分。
  • 具备委托的并发系统须要处理服务错误,并有原则地从错误中恢复。这类服务的客户端须要知道,在从新启动过程当中,任务/消息可能会丢失。即便不发生丢失,因为先前排队的任务(长队列)、垃圾收集形成的延迟等缘由,响应也可能被任意延迟。面对这些问题,并发系统应该以超时的形式处理响应截止期,就像网络/分布式系统同样。

接下来,让咱们看看如何使用角色模型来克服这些挑战。

有什么讨论的内容,能够加我公众号:

相关文章
相关标签/搜索