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

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

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信号机制需要被引入。失败成为领域模型的一部分。任务委派的并发系统需要处理服务故障并且有原则性的方法恢复它们。这种服务的客户端需要知道任务/消息会在重启中丢失。即使不丢失,一个响应或许会由于队列 (一个很长的队列) 中先前的任务而发生任意的延迟,由垃圾回收造成的延迟等等。在这些情况下,并发系统应该以超时的形式对待响应截止时间,就像网络/分布式系统一样。

以上就是详解为什么现代系统需要一个新的编程模型的详细内容,更多关于现代系统需要一个新的编程模型的资料请关注我们其它相关文章!

(0)

相关推荐

  • MongoDB中MapReduce编程模型使用实例

    注:作者使用的MongoDB为2.4.7版本. 单词计数示例: 插入用于单词计数的数据: 复制代码 代码如下: db.data.insert({sentence:'Consider the following map-reduce operations on a collection orders that contains documents of the following prototype'})db.data.insert({sentence:'I get the following e

  • Spring 注解编程模型相关知识详解

    Spring 中有一个概念叫「元注解」(Meta-Annotation),通过元注解,实现注解的「派生性」,官方的说法是「Annotation Hierarchy」. 什么是元注解 所谓元注解,即标注在注解上的注解.这种方式所形成的注解层级结构中,元注解在层级结构的上面,我叫它父注解(Super Annotation), 被注解的注解在层级结构的下面,叫它子注解(Sub Annotation).引入元注解的目的是为了实现属性重写(Attribute Override) 的目的. 举个简单的例子:

  • 用PHP编程开发“虚拟域名”系统

    如果自己的服务器也能够实现简记域名就好了.其实这并不复杂.你也可以做一个简记域名系统. 简记域名系统的关键技术在于:实现Web页面的重定向(Redirctory).在本质上,简记域名系统和虚拟机系统完全不同.虚拟机的虚拟域名和IP是存在一一对应关系的.而简记域名系统不需要将域名和IP做一一映射.也就是说,它根本不需要复杂的域名解析机制和虚拟机来完成,它所做的事情就是当你在请求yourname.somedomain时,将你的浏览器重新定向到你本来存放Html页面的地方. 本程序运行环境是:RedH

  • 编程用到的windows系统目录变量简写

    %ALLUSERSPROFILE% C:\Documents and Settings\All Users %USERPROFILE% C:\Documents and Settings\当前用户名 %HOMEPATH% C:\Documents and Settings\当前用户名 %SYSTEMROOT% C:\WINDOWS %WINDIR% C:\WINDOWS %ComSpec% C:\WINDOWS\System32\cmd.exe %APPDATA% C:\Documents an

  • python多线程抽象编程模型详解

    最近需要完成一个多线程下载的工具,对其中的多线程下载进行了一个抽象,可以对所有需要使用到多线程编程的地方统一使用这个模型来进行编写. 主要结构: 1.基于Queue标准库实现了一个类似线程池的工具,用户指定提交任务线程submitter与工作线程worker数目,所有线程分别设置为后台运行,提供等待线程运行完成的接口. 2.所有需要完成的任务抽象成task,提供单独的无参数调用方式,供worker线程调用:task以生成器的方式作为参数提供,供submitter调用. 3.所有需要进行线程交互的

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

    为什么现代系统需要一个新的编程模型? Actor模型作为一种高性能网络中的并行处理方式由Carl Hewitt几十年前提出-高性能网络环境在当时还不可用.如今,硬件和基础设施的能力已经赶上并超越了Hewitt的愿景.因此,高要求的分布式系统的建造者遇到了不能完全由传统的面向对象编程(OOP)模型解决的挑战,但这可以从Actor模型中获益. 今天,Actor模型不仅被认为是高效的解决方案--这已经被世界上要求最高的应用所检验.为了突出Actor模型解决的问题,这个主题讨论以下传统编程的假设与现代多

  • 详解如何用JavaScript编写一个单元测试

    目录 为什么要进行单元测试? 范围界定和编写单元测试 保持单元测试简短而简单 考虑正面和负面的测试用例 分解长而复杂的函数 避免网络和数据库连接 如何编写单元测试 创建一个新项目 实现一个类 配置和添加我们的第一个单元测试 添加更多单元测试 修复错误 最后 测试代码是确保代码稳定的第一步.能做到这一点的最佳方法之一就是使用单元测试,确保应用程序中的每个较小的功能都按应有的方式运行——尤其是当应用程序接收到极端或无效输入,甚至可能有害的输入时. 为什么要进行单元测试? 进行单元测试有许多不同的方法

  • 详解Android获得系统GPU参数 gl.glGetString

    详解Android获得系统GPU参数 gl.glGetString 通过文档的查找,以及源码的剖析,Android的GPU信息需要通过OpenGL来获取,android framework层提供GL10来获取相应的参数,而GL10要在使用自定义的View时才可以获得,下面是获得GPU信息的例子: 1.实现Render类 class DemoRenderer implements GLSurfaceView.Renderer { public void onSurfaceCreated(GL10

  • 详解.NET Core 3.0 里新的JSON API

    为什么需要新的 JSON API ? JSON.NET  大家都用过,老版本的 ASP.NET Core 也依赖于 JSON.NET . 然而这个依赖就会引起一些版本问题:例如 ASP .NET  Core某个版本需要使用 JSON .NET  v10 ,而另一个库需要使用 JSON.NET  v11 :或者 JSON .NET   出现了一个新版本,而ASP .NET Core 还不能支持这个版本,而您却想使用该版本. System.Text.Json   随着 NET Core  3.0 的

  • 详解Python 3.10 中的新功能和变化

    随着最后一个alpha版发布,Python 3.10 的功能更改全面敲定! 现在,正是体验Python 3.10 新功能的理想时间!正如标题所言,本文将给大家分享Python 3.10中所有重要的功能和更改. 新功能1:联合运算符 在过去, |符号用于 "算术或"运算,例如: print(0 | 0) print(0 | 1) print({1, 2} | {2, 3}) 输出: 0 1 {1, 2, 3} 在Python 3.10中, |符号有的新语法,可以表示x类型 或 Y类型,以

  • 详解用Docker快速搭建一个博客网站

    目录 一.准备工作 二.部署流程  三.访问测试 Halo 是一款现代化的个人独立博客系统,给习惯写博客的同学多一个选择. 官网地址:https://halo.run/ 一.准备工作 本章教程基于Docker搭建,所以需要你提前在服务器上安装好Docker环境. Docker安装教程:https://www.jb51.net/article/94067.htm 二.部署流程 (1)创建工作目录 mkdir ~/.halo && cd ~/.halo (2)下载配置文件到工作目录 wget

  • 详解如何用js实现一个网页版节拍器

    目录 引言 1. 需求分析 2. 素材准备 3. 开发实现 3.1 框架选型 3.2 模块设计 3.3 数据结构设计 3.4 播放逻辑 3.5 音频控制 3.6 动效 3.7 大屏展示 3.8 新增人声发音 4. 部署 5. 后续工作 5.1 目前存在的问题 ios声音 5.2 TODO 切换不同音效 引言 平时练尤克里里经常用到节拍器,突发奇想自己用js开发一个. 最后实现的效果如下:ahao430.github.io/metronome/. 代码见github仓库:github.com/ah

  • 详解如何使用C++写一个线程安全的单例模式

    目录 单例模式的简单实现 有问题的双重检测锁 现代C++中的解决方法 使用现代C++中的内存顺序限制 使用现代C++中的call_once方法 使用静态局部变量 单例模式的简单实现 单例模式大概是流传最为广泛的设计模式之一了.一份简单的实现代码大概是下面这个样子的: class singleton { public: static singleton* instance() { if (inst_ != nullptr) { inst_ = new singleton(); } return i

  • 详解用Node.js写一个简单的命令行工具

    本文介绍了用Node.js写一个简单的命令行工具,分享给大家,具体如下: 操作系统需要为Linux 1. 目标 在命令行输入自己写的命令,完成目标任务 命令行要求全局有效 命令行要求可以删除 命令行作用,生成一个文件,显示当前的日期 2. 代码部分 新建一个文件,命名为sherryFile 文件sherryFile的内容 介绍: 生成一个文件,文件内容为当前日期和创建者 #! /usr/bin/env node console.log('command start'); const fs = r

  • 详解JS实现系统登录页的登录和验证

    这篇文章用JS显示表单的登录以及验证和对键盘的监听,这里有两种方法,一种是无需用户验证直接登录,一种是需要账户密码匹配才可登录. 1. html代码 <div class="content"> <div class="login-wrap"> <form id="user_login" action=""> <h3>登 录</h3> <input class=&

随机推荐