摘要:如今高要求的分布式系统的建造者遇到了不能完全由传统的面向对象编程(OOP)模型解决的挑战,但这可以从Actor模型中获益。

为什么现代系统需要一个新的编程模型?

Actor模型作为一种高性能网络中的并行处理方式由Carl Hewitt几十年前提出-高性能网络环境在当时还不可用。如今,硬件和基础设施的能力已经赶上并超越了Hewitt的愿景。因此,高要求的分布式系统的建造者遇到了不能完全由传统的面向对象编程(OOP)模型解决的挑战,但这可以从Actor模型中获益。

今天,Actor模型不仅被认为是高效的解决方案——这已经被世界上要求最高的应用所检验。为了突出Actor模型解决的问题,这个主题讨论以下传统编程的假设与现代多线程、多CPU体系架构之间的不匹配:

  • 封装的挑战
  • 现代计算机体系结构中共享内存的错觉
  • 一个调用栈的错觉

封装的挑战

OOP的一个核心支柱是封装。封装表明一个对象的内部状态不能直接从外部访问;它只可以通过调用一组辅助的方法修改。对象负责暴露保护它所封装数据的不变性的安全操作。例如,在一个有序二叉树上的操作不允许违反树的有序性。调用者希望保持有序性,当查询树上一条特定的数据时,它们需要能够依赖这个约束。

当分析OOP运行时的行为时,我们有时候画出一个消息序列图展示方法调用的交互过程。例如:

不幸的是,上面的图表没能精确表示执行过程中对象的生命线。实际上,一个线程执行所有的调用,所有对象的不变体约束出现在同一个方法被调用的线程中。更新线程执行图,它看起来是这样:

当试图对多线程行为建模时,上面阐述的重要性变得明显了。突然,我们画出的简洁的图表变得不够充分了。我们可以尝试解释多线程访问同一对象:

有一个执行部分,两个线程调用同一个方法。不幸的是,对象的封装模型不能保证执行这部分时会发生什么。两个线程之间没有某种协调的话,两个调用指令将以不能保证不变体性质的任意方式相互交错。现在,想象一下这个由多个线程存在而变得复杂的问题。

解决这个问题的常见方法是给这些方法加一个锁。尽管这保证了在给定的时间内最多一个线程将执行该方法,但是这是一个代价高昂的策略: 

  • 锁严重限制了并发,锁在现代CPU体系结构中的代价很高,要求操作系统承担挂起线程并随后恢复它的重负。
  • 调用者线程被阻塞,因此它不能做其他有意义的工作。在桌面应用中这是不能接受的,我们希望使应用程序的用户界面(UI)即使在一个很长的后台作业正在运行的时候也是可响应的。在后台,阻塞是完全浪费的。或许有人想到这可以通过开启一个新线程弥补,但线程也是一个代价高昂的抽象。
  • 锁引入了一个新的威胁:死锁

这些事实导致一个无法取胜的局面:

  • 没有足够的锁,状态会被破坏
  • 有足够的锁,性能受损并很容易导致死锁

另外,锁只有在本地有用。当涉及跨机器协调时,唯一可选的是分布式锁。不幸的是,分布式锁比本地锁低效几个数量级,并且限制了伸缩性。分布式锁协议需要在网络中跨机器的多轮通信,因此延迟飞涨。

在面向对象语言中,我们通常很少考虑线路或线性执行路径。我们经常把系统想象成一个对象实例的网络,这些实例对象响应方法调用、修改自身内部状态、然后通过方法调用相互通信以驱动整个应用状态向前:

然而,在一个多线程的分布式环境中,实际发生的是线程沿着方法调用贯穿这个对象实例网络。因此,线程是真正的运行驱动者:

【总结】

  • 对象只能在单线程访问时保证封装(不变体的保护),多线程执行几乎总会导致破坏对象内部状态。每个不变体可以被处于同一代码段相互竞争的两个线程违反。
  • 虽然锁似乎是对维护多线程时的封装很自然的补救,实际上,在任何现实应用中锁很低效并很容易导致死锁。
  • 锁在本地有用,但试图使锁成为分布式的,可以提供有限潜力的扩展。

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

80-90年代的编程模型定义:写入一个变量意味着直接写到内存位置 (这在一定程上混淆了局部变量可能仅存在于寄存器)。在现代体系架构中,如果我们简化一下,CPUs会写到cache行而不是直接写入内存。大多数caches是CPU局部私有的,也就是,一个核写入变量不会被其他核看到。为了使局部改变对其他核可见,因此对于另一个线程,cache行需要被传送到其他核的cache。

在JVM中,我们必须通过使用volatile或Atomic显式地指示线程间共享的内存位置。否则,我们只能在锁定的部分访问这些内存。为什么我们不将所有变量标记为volatile?因为跨核传送cache行是一个代价非常高昂的操作!这样做会隐式地停止涉及做额外工作的核,并导致缓存一致性协议的瓶颈。(CPUs用于主存和其他CPUs之间传输cache行的协议)。结果便是降低数量级的运行速度。

即使对于了解这个情况的开发者,搞清楚哪个内存位置应该被标记为volatile或者使用哪一种原子结构是一门黑暗的艺术。

【总结】

  • 没有真正的共享内存了,CPU核就像网络中的计算机一样,将数据块(cache行)显式地传送给彼此。CPU之间的通信和网络中计算机之间通信的相同之处比许多人意识到的要多。传送消息是如今跨CPUs或网络中计算机的标准。
  • 相对于通过标记为共享或使用原子数据结构的变量来隐藏消息传递的层面,一个更规范和有原则的方法是保存状态到一个并发实体本地并通过消息显式地在并发实体间传送数据或事件。

一个调用栈的错觉

今天,我们常常将调用栈视为理所当然。但是,调用栈是在一个并发程序不那么重要的时代发明的,因为多CPU系统那时不常见。调用栈没有跨越线程,因而没有对异步调用链建模。

当一个线程意图委派一个任务给后台的时候会出现问题。实际上,这意味着委托给另一个线程。这不是一个简单的方法、函数调用,因为调用严格上属于线程内部。通常,调用者(caller)线程将一个对象放入与一个工作线程(callee)共享的内存位置,反过来,这个工作线程(callee)在某个循环事件中获取这个对象。这使得调用者(caller)线程可以向前运行和执行其他任务。

第一个问题是:调用者(caller)线程如何被通知任务完成了?但是当一个任务失败且带有异常的时候一个更严重问题出现了。异常应该传播到哪里?异常将被传播到工作者(worker)线程的异常处理器而完全忽略谁是真正的调用者(caller):

这是一个严重的问题。工作者(worker)线程如何处理这种情况?它可能无法解决这个问题,因为它通常不知道失败任务的目的。调用者(caller)线程需要以某种方式被通知,但是没有调用栈去返回一个异常。失败通知只能通过边信道完成,例如,将一个错误代码放在调用者(caller)线程原本期待结果准备好的地方。如果这个通知不到位,调用者(caller)线程不会被通知任务失败和丢失!这和网络系统的工作方式惊人地相似-网络系统中的消息和请求可以丢失或失败而没有任何通知。

在任务出错和一个工作者(worker)线程遇到一个bug并不可恢复的时候,这个糟糕的情况会变得更糟。例如,一个由bug引起的内部异常向上传递到工作者(worker)线程的根部并使该线程关闭。这立即产生一个疑问,谁应该重启由该线程持有的这一服务的正常操作,以及怎样将它恢复到一个已知的良好状态?乍一看,这似乎很容易,但是我们突然遇到一个新的、意外的现象:线程正在执行的实际任务已经不在任务被取走得共享内存位置了 (通常是一个队列)。事实上,由于异常到达顶部,展开所有的调用栈,任务状态完全丢失了!我们已经丢失了一条消息,尽管这是本地的通信,没有涉及到网络 (消息丢失是可期望的)。

【总结】

  • 为了在当下系统实现有意义的并发和性能,线程必须以一种高效的、无阻塞的方式相互委派任务。有了这种任务委派并发方式(网络/分布式计算更是如此),基于栈调用的error处理失效了,新的、显式的error信号机制需要被引入。失败成为领域模型的一部分。
  • 任务委派的并发系统需要处理服务故障并且有原则性的方法恢复它们。这种服务的客户端需要知道任务/消息会在重启中丢失。即使不丢失,一个响应或许会由于队列 (一个很长的队列) 中先前的任务而发生任意的延迟,由垃圾回收造成的延迟等等。在这些情况下,并发系统应该以超时的形式对待响应截止时间,就像网络/分布式系统一样。

本文翻译自https://doc.akka.io/docs/akka/current/guide/actors-motivation.html

本文分享自华为云社区《【Akka系列】之 为什么现代系统需要一个新的编程模型? 》,原文作者:荔子 。

点击关注,第一时间了解华为云新鲜技术~

为什么现代系统需要一个新的编程模型?相关推荐

  1. 为计算机创建一个新的用户,win10系统创建一个新账户的解决步骤

    win10系统使用久了,好多网友反馈说关于对win10系统创建一个新账户设置的方法,在使用win10系统的过程中经常不知道如何去对win10系统创建一个新账户进行设置,有什么好的办法去设置win10系 ...

  2. SAP ABAP 平台新的编程模型

    ABAP 编程语言的演变 在过去 40 多年中开发的所有 SAP 功能中,大部分都是用 ABAP 编写的.ABAP 编程语言是我们的旗舰语言,并且已经证明它是开发业务应用程序的经过验证的强大平台. 多 ...

  3. 操原作业(一)Ubuntu系统编译一个新的内核

    操作系统原理这门课布置了一项作业,要求在Ubuntu系统中编译一个新的内核.下面介绍怎么在Ubuntu系统中编译一个新的内核. 安装Ubuntu系统 如何安装win10+Ubuntu双系统,我已经在上 ...

  4. THRUST:一个开源的、面向异构系统的并行编程语言:编程模型主要包括:数据并行性、任务并行性、内存管理、内存访问控制、原子操作、同步机制、错误处理机制、混合编程模型、运行时系统等

    作者:禅与计算机程序设计艺术 1.简介 https://github.com/NVIDIA/thrust 2021年8月,当代科技巨头Facebook宣布其开发了名为THRUST的高性能计算语言,可用 ...

  5. aio 系统原理 Java_Java新一代网络编程模型AIO原理及Linux系统AIO介绍

    从JDK 7版本开始,Java新加入的文件和网络io特性称为nio2(new io 2, 因为jdk1.4中已经有过一个nio了),包含了众多性能和功能上的改进,其中最重要的部分,就是对异步io的支持 ...

  6. 为未来创建一个新的财务模型,分享逸管家互联网共享红利

    资本是企业生存的重要因素.在.的小企业的长期发展中,从发展的初始阶段到发展的中间阶段,外部资本注入需要长期生存. 金融市场是资本市场的重要组成部分.实体经济的可持续健康发展离不开完善的金融环境.中国小 ...

  7. Q146:PBRT-V3,对系统进行拓展(以添加一个新的Integrator为例)

    本文的主要内容是:给PBRT-V3系统添加一个新的Integrator(比如,vcm),确保在编译PBRT-V3时能够编译到新添加的vcm文件,并能编译成功. 不含vcm的具体实现(必须实现的函数全部 ...

  8. 实验六:分析Linux内核创建一个新进程的过程

    20135108 李泽源 阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h ...

  9. 操原上机(一) 在Linux系统中增加新的系统调用

    在LINUX中增加新的系统调用 编写新的系统调用函数(指函数实现部分) 注册新的系统调用(声明系统调用函数和编号) 编译新LINUX内核 编译和安装模块 启动新的LINUX内核 编写应用程序测试新的系 ...

最新文章

  1. 2018-3-10论文(网络评论中非结构化信息表示与应用研究)-----综合评价的实例
  2. 3.4 改进集束搜索-深度学习第五课《序列模型》-Stanford吴恩达教授
  3. Intel Realsense 通过用户配置文件(profile)获取深度传感器(depth_sensor)超蛋疼的一幕 dir()
  4. 浅谈以太坊智能合约的设计模式与升级方法
  5. 加载spring上下文的多种方式总结
  6. 并查集练习(0743) SWUST OJ
  7. Complete Tripartite CodeForces - 1228D(三分图染色)
  8. hough变换检测圆周_Python OpenCV 霍夫变换
  9. #ifndef#define与namespace杂谈
  10. 【CF949D】Curfew(贪心)
  11. Professional ASP.NET 2.0之跨页提交-Cross Page Posting
  12. 安装neptune-client库
  13. 网约车管理系统源码 打车APP源码 顺风车源码
  14. C语言中 1%3,算术什么意思啊 算数什么意思
  15. 郑州大学计算机考研拟录取名单,郑州大学2017年统考硕士生拟录取名单2(17)
  16. 下载软件一直转圈圈_苹果手机下载不了app,一直转圈怎么办?(附两种解决方法)...
  17. python如何拼读英语单词怎么写_浅谈如何拼读英语新单词
  18. 资料员报考建筑八大员报考建筑资料员工程竣工资料整理的举措
  19. Python实现门禁管理系统(源码)
  20. python异常处理_Python异常处理

热门文章

  1. java面向对象程序课本,Java面向对象程序设计
  2. java csv 双引号_Java-使用Scess编写CSV时从字符串类型数据中删除双引号
  3. 软件工程测试旅游管理系统,旅游管理系统的设计与实现
  4. python每日一题今天的答案_python每日一题总结1
  5. isdigit()、isalpha()、isalnum() 三个函数的区别和注意点
  6. 【综合】JS跨域方案JSONP与CORS跨域
  7. 12、scala函数式编程集合
  8. 2-14 三级菜单
  9. 【C#】隐式类型var
  10. 将深度缓冲z值变换到相机坐标系