摘要:现在高要求的分布式系统的建造者遇到了不能彻底由传统的面向对象编程(OOP)模型解决的挑战,但这能够从Actor模型中获益。
Actor模型做为一种高性能网络中的并行处理方式由Carl Hewitt几十年前提出-高性能网络环境在当时还不可用。现在,硬件和基础设施的能力已经遇上并超越了Hewitt的愿景。所以,高要求的分布式系统的建造者遇到了不能彻底由传统的面向对象编程(OOP)模型解决的挑战,但这能够从Actor模型中获益。
今天,Actor模型不只被认为是高效的解决方案——这已经被世界上要求最高的应用所检验。为了突出Actor模型解决的问题,这个主题讨论如下传统编程的假设与现代多线程、多CPU体系架构之间的不匹配:html
OOP的一个核心支柱是封装。封装代表一个对象的内部状态不能直接从外部访问;它只能够经过调用一组辅助的方法修改。对象负责暴露保护它所封装数据的不变性的安全操做。例如,在一个有序二叉树上的操做不容许违反树的有序性。调用者但愿保持有序性,当查询树上一条特定的数据时,它们须要可以依赖这个约束。
当分析OOP运行时的行为时,咱们有时候画出一个消息序列图展现方法调用的交互过程。例如:编程
不幸的是,上面的图表没能精确表示执行过程当中对象的生命线。实际上,一个线程执行全部的调用,全部对象的不变体约束出如今同一个方法被调用的线程中。更新线程执行图,它看起来是这样:缓存
当试图对多线程行为建模时,上面阐述的重要性变得明显了。忽然,咱们画出的简洁的图表变得不够充分了。咱们能够尝试解释多线程访问同一对象:安全
有一个执行部分,两个线程调用同一个方法。不幸的是,对象的封装模型不能保证执行这部分时会发生什么。两个线程之间没有某种协调的话,两个调用指令将以不能保证不变体性质的任意方式相互交错。如今,想象一下这个由多个线程存在而变得复杂的问题。网络
解决这个问题的常见方法是给这些方法加一个锁。尽管这保证了在给定的时间内最多一个线程将执行该方法,可是这是一个代价高昂的策略: 数据结构
这些事实致使一个没法取胜的局面:多线程
另外,锁只有在本地有用。当涉及跨机器协调时,惟一可选的是分布式锁。不幸的是,分布式锁比本地锁低效几个数量级,而且限制了伸缩性。分布式锁协议须要在网络中跨机器的多轮通讯,所以延迟飞涨。架构
在面向对象语言中,咱们一般不多考虑线路或线性执行路径。咱们常常把系统想象成一个对象实例的网络,这些实例对象响应方法调用、修改自身内部状态、而后经过方法调用相互通讯以驱动整个应用状态向前:并发
然而,在一个多线程的分布式环境中,实际发生的是线程沿着方法调用贯穿这个对象实例网络。所以,线程是真正的运行驱动者:异步
【总结】
80-90年代的编程模型定义:写入一个变量意味着直接写到内存位置 (这在必定程上混淆了局部变量可能仅存在于寄存器)。在现代体系架构中,若是咱们简化一下,CPUs会写到cache行而不是直接写入内存。大多数caches是CPU局部私有的,也就是,一个核写入变量不会被其余核看到。为了使局部改变对其余核可见,所以对于另外一个线程,cache行须要被传送到其余核的cache。
在JVM中,咱们必须经过使用volatile或Atomic显式地指示线程间共享的内存位置。不然,咱们只能在锁定的部分访问这些内存。为何咱们不将全部变量标记为volatile?由于跨核传送cache行是一个代价很是高昂的操做!这样作会隐式地中止涉及作额外工做的核,并致使缓存一致性协议的瓶颈。(CPUs用于主存和其余CPUs之间传输cache行的协议)。结果即是下降数量级的运行速度。
即便对于了解这个状况的开发者,搞清楚哪一个内存位置应该被标记为volatile或者使用哪种原子结构是一门黑暗的艺术。
【总结】
今天,咱们经常将调用栈视为理所固然。可是,调用栈是在一个并发程序不那么重要的时代发明的,由于多CPU系统那时不常见。调用栈没有跨越线程,于是没有对异步调用链建模。
当一个线程意图委派一个任务给后台的时候会出现问题。实际上,这意味着委托给另外一个线程。这不是一个简单的方法、函数调用,由于调用严格上属于线程内部。一般,调用者(caller)线程将一个对象放入与一个工做线程(callee)共享的内存位置,反过来,这个工做线程(callee)在某个循环事件中获取这个对象。这使得调用者(caller)线程能够向前运行和执行其余任务。
第一个问题是:调用者(caller)线程如何被通知任务完成了?可是当一个任务失败且带有异常的时候一个更严重问题出现了。异常应该传播到哪里?异常将被传播到工做者(worker)线程的异常处理器而彻底忽略谁是真正的调用者(caller):
这是一个严重的问题。工做者(worker)线程如何处理这种状况?它可能没法解决这个问题,由于它一般不知道失败任务的目的。调用者(caller)线程须要以某种方式被通知,可是没有调用栈去返回一个异常。失败通知只能经过边信道完成,例如,将一个错误代码放在调用者(caller)线程本来期待结果准备好的地方。若是这个通知不到位,调用者(caller)线程不会被通知任务失败和丢失!这和网络系统的工做方式惊人地类似-网络系统中的消息和请求能够丢失或失败而没有任何通知。
在任务出错和一个工做者(worker)线程遇到一个bug并不可恢复的时候,这个糟糕的状况会变得更糟。例如,一个由bug引发的内部异常向上传递到工做者(worker)线程的根部并使该线程关闭。这当即产生一个疑问,谁应该重启由该线程持有的这一服务的正常操做,以及怎样将它恢复到一个已知的良好状态?乍一看,这彷佛很容易,可是咱们忽然遇到一个新的、意外的现象:线程正在执行的实际任务已经不在任务被取走得共享内存位置了 (一般是一个队列)。事实上,因为异常到达顶部,展开全部的调用栈,任务状态彻底丢失了!咱们已经丢失了一条消息,尽管这是本地的通讯,没有涉及到网络 (消息丢失是可指望的)。
【总结】
本文翻译自https://doc.akka.io/docs/akka/current/guide/actors-motivation.html
本文分享自华为云社区《【Akka系列】之 为何现代系统须要一个新的编程模型? 》,原文做者:荔子 。