官方链接

小结:

本文从传统面向对象的封装,内存共享以及并发错误处理等几个方面对比了传统并发模型的缺陷。

  • 对象封装在并发的情况下很难保证原子性,加锁又会导致性能问题,还会导致死锁
  • 多核CPU在cache-lines上的资源同步很容易造成性能问题(使用volatile或者atomic)。
  • 传统的线程模型会因为错误处理导致消息丢失,难以恢复。

简述

通过对比actor模型和传统模型,更好的理解actor模型在高并发的分布式场景中的优势.

Actor模型在十多年前被Carl Hewitt提出用来在高性能网络环境中处理并发问题--在当时的环境没有价值.如今硬件和基础设施能力赶上了甚至超过了Carl Hewitt的预见.因此当组织要构建苛刻需求(demanding requirements)的分布式系统遇到了到了挑战,我们不能够完全通过传统的面向对象模型解决这些问题,但是使用actor模型却能够从中受益.

如今,actor模型不仅被认识为是一个高效的解决方案-他也在很多世界上需求苛刻的应用中被证明.

为了突出显示actor模型解决的问题,下面将讨论现代多线程多CPU与传统编程的观点不匹配的地方.

封装的挑战

面向对象的一个核心概念就是 封装(encapsulation) .封装规定内部数据不能够直接的被外部访问.只能够通过暴漏的方法访问.对象需要负责暴露安全的操作来保证他的数据的不变性质.

举个例子在一个有序二叉树实现上的操作,不能够允许违反树排序不变的. 调用方期望树是完好的, 并且在查询树的某一数据段时,必须依赖这个约束.

当我们分析面向对象的实时行为,我们可以通过时序图来展示方法调用.

不幸的是,上面的图不能够确切的代表实例在执行时的生命线.实际中,一个线程执行这些调用,当不变量的执行发生在调用的同一个线程.更新上面的图

当我们试着模拟多线程中发生的事情,使用这种方式会变得清晰.突然我们整齐的图变得零乱.我们可以试着说明多线程是如何访问同一个实例:

上图表明两个线程访问同一个方法.不幸的是,封装对象的模型不能够保证在这个情况下发生什么. 两个方法调用可以以任意方式进入,所以在没有特定的协助下我们不能保证不变量不变了.

通常解决这个问题的方法是添加一个锁.这可以保证同一时刻只有一个线程进入这个方法,但是代价非常昂贵:

  • 锁严格限制了并发,在当前CPU架构下会非常的昂贵,需要一个操作系统的繁重的操作(heavy-lifting)来挂起一个线程并且一会恢复.
  • 调用线程会被阻塞,所以它不能做其他有意义的工作. 即使在桌面程序这也是不可以被接受的,我们希望即使后台做了一个很长时间的工作 UI也是能够相应的.在后端,阻塞也是非常浪费资源的,即使另起一个线程来弥补,但是线程就是一个昂贵的抽象
  • 锁也会带来死锁的问题.

这些问题会导致一个必败的情况:

  • 没有足够的锁,状态获取将被破坏.
  • 使用了太多的锁,性能不行还容易造成死锁.

额外的,锁只能在本地工作,当我们需要多台机器协作时,唯一的解法是分布式锁.分布式锁性能比本地锁差了很多而且对扩展限制比较严格. 分布式锁协议需要多台机器多次得网络往返通信,所以延迟会飞涨(latency goes through the roof)

在面向对象的语言我们很少考虑线程或者线性执行路径.我们通常把一个系统想象成一个对象实例的网络,对方法调用做出反应,修改他们内部状态,然后互相通信驱动着整个应用状态向前.

下图是对象通过方法相互调用

然而在多线程的分布式环境中,实际发生的是线程通过方法调用"穿过了"对象实例的网络.因此,线程才是真正

的驱动执行的东西

下图三个颜色 三个线程中方法互相调用.

 总结:

  • 面对单线程的访问,对象可以只通过封装来保证不变量的保护,多线程执行通常回破坏内部状态.每个不变量在同一个代码片段可能被两个互相竞争的线程侵犯.
  • 锁是对于多线程中 似乎是天然的改进来支持封装.实际中它是低效的容易引起死锁的.
  • 锁在本地工作,如果想让他们做成分布式,会使扩展性受到限制.

在现代计算机架构共享内存的问题

80-90年代的概念写入变量意味着直接写到内存中(局部变量也许之存在寄存器中).现代的架构.CPUs 将变量写在cache lines中. 大部分的缓存都是在CPU 核心中,多以会出现一个核心对其他的不可见.为了使局部改变对于其他核心可见,从而让别的线程可见,缓存行需要将改变传到别的核心缓存中.

在JVM,我们不得不通过使用 volatile关键字或者 原子的修饰来明确标记存储是要跨线程共享的.因此,我们只能通过锁定区域来访问他们。因为calche lines 跨核心是非常昂贵的操作,所以我们不能够把所有的变量都标记为volatile. 如果这么做将隐式的暂停了相关参与核心做额外的工作,并且造成缓存一致性协议(MESI)的瓶颈(cpus 通常使用这个协议在主存和其他cpu之间传输cacle lines).最终导致效率数量级的下降.

总结:

  • cpu把数据块(cache lines)明确的传递给彼此 就如同计算机在网络上做的一样.Inter-CPU 通信和网络通信又许多共同点.传递消息已经变成了一种常态,无论跨Cpu还是网络上的计算机.
  • 与在标记变量是共享的或者使用原子数据结构不同,一种更有纪律和原则的方法可以将状态保持为并发实体的本地状态,然后通过消息在并发实体中传播数据或者事件。

调用堆栈的问题

如今,我们经常把调用堆栈当作理所当然.但是,他们是在多CPU系统不流行的时代被发明的.调用堆栈不能跨线程所以不能模拟异步调用链.

这个问题在一个线程打算委托一个任务给"backgroud"出现.实际中,这意味着委托给其他线程.这种方式不是一个简单得方法/函数调用,因为这种调用是严格的在线程本地.通常发生,"调用者"把一个对象放在一个和工作线程("callee")共享得内存位置, 而调用者又在某些事件循环中获取它.这允许"caller"线程继续或者去做别的任务.

第一个问题是,怎么样通知"caller"任务已经完成了? 与之而来的一个更严重的问题出现,当一个任务以异常状态失败了.怎么把异常传播过去?它将传播给工作线程的异常处理器完全的忽略谁是实际的"caller": 

这是个严肃的问题.工作线程怎么处理这个问题呢? 它似乎不能解决这个问题因为它通常不知道失败任务的目的.调用线程需要以某种方式被通知,但是没有调用堆栈去释放异常.失败的通知只能够被别的通道完成,比如把一个错误码放到调用者线程期望获取结果的位置。如果这个通知没有到位,调用者永远不能够获得失败的通知,这个任务就丢失了! 这个和网络系统工作的很相似,消息/请求可能丢失/失败 而没有任何通知.

更坏的情况是当真的出现了错误并且一个工作线程遇到了bug或者一个不可恢复的情况。比如,一个内部一场造成了一个bug冒出来到线程的root 并且使线程关闭了. 这时谁应该重启这个线程承载的服务的正常运行,并且怎么重置成一个好的状态? 乍一看,貌似可以被管控,但我们忽然要面对一个新的,不期望的现象(phenomenon):当前线程正在工作的实际任务,已经不在共享内存的位置-任务获取的地方(通常是队列). 事实上,由于异常到达了最上级,释放了所有的堆栈,这个任务状态已经完全丢失了! 我们甚至没有网络的参与就在本地通信中丢失了消息.

总结:

  • 为了让当前系统实现并发和高性能,线程之间必须以一个高效的没有阻塞的方式相互委托任务.使用这种任务委托并发性(网络/分布式计算 更是如此), 基于堆栈调用的错误处理将会崩溃. 一个新的,明确的错误通知机制需要被引入.错误变成了领域模型的一部分.
  • 使用工作委托的并发系统需要处理服务失败并且有一个恢复他们的方式。这些服务的客户端需要察觉到任务/消息也许在重启的时候丢失. 就算这个丢失没有发生,一个响应 可能会因为在队列之前的排队的任务给延时,比如gc导致的延时.面对这些,并发系统应该以超时的形式处理响应的最后期限,就想网络/分布式系统.

Actor模型与传统模型相关推荐

  1. python 多分类情感_文本情感分类(一):传统模型

    前言:四五月份的时候,我参加了两个数据挖掘相关的竞赛,分别是物电学院举办的"亮剑杯",以及第三届 "泰迪杯"全国大学生数据挖掘竞赛.很碰巧的是,两个比赛中,都有 ...

  2. 推荐系统中传统模型——LightGBM + FFM融合

    之前比较相关的文章: 推荐系统中传统模型--LightGBM + LR融合 python - 机器学习lightgbm相关实践 1 深入FFM原理与实践 来自美团技术团队的,深入FFM原理与实践 FM ...

  3. Actor模型和CSP模型的区别

    Akka/Erlang的actor模型与Go语言的协程Goroutine与通道Channel代表的CSP(Communicating Sequential Processes)模型有什么区别呢? 首先 ...

  4. 软件开发传统模型——瀑布模型、原型模型、增量模型、螺旋模型、喷泉模型

    软件开发模型: 定义: 软件开发的全部过程.活动和任务的结构框架,通过该模型能清晰.直观地表达软件开发全过程,明确地规定要完成的主要活动和任务,它奠定了软件项目工作的基础. 其中最为代表的就有此五类模 ...

  5. DDD领域驱动设计 — 贫血模型与充血模型

    文章转载来源:https://juejin.cn/post/6917125801460629518 | 前言  要想深入掌握和了解 DDD 领域驱动设计的核心,那无论如何也绕不开两大较为抽象的概念-- ...

  6. DDD 领域驱动设计:贫血模型、充血模型的深入解读!

    作者:JavaEdge在掘金 链接:https://juejin.cn/post/6917125801460629518 -     前言     - 要想深入掌握和了解 DDD 领域驱动设计的核心, ...

  7. 机器学习判定模型与生成模型区别

    作者:politer 链接:https://www.zhihu.com/question/20446337/answer/256466823 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权, ...

  8. BAT面试题9:谈谈判别式模型和生成式模型?

    BAT面试题9:谈谈判别式模型和生成式模型? https://mp.weixin.qq.com/s/X7zWJCMN7gbCwqskIIpLcw 判别方法:由数据直接学习决策函数 Y = f(X),或 ...

  9. 并发模型与IO模型梳理

    并发模型 常见的并发模型一般包括3类,基于线程与锁的内存共享模型,actor模型和CSP模型,其中尤以线程与锁的共享内存模型最为常见.由于go语言的兴起,CSP模型也越来越受关注.基于锁的共享内存模型 ...

最新文章

  1. 知识图谱在互联网金融中的应用
  2. Citrix VDI攻略之四:PVS安装及配置
  3. 给实例动态增加方法VS给类动态增加方法
  4. redis-cluster集群单机搭建
  5. 云服务器安装其他版本系统,云服务器安装自己的系统版本
  6. 最大概率法分词及性能測试
  7. 【Linux】完美解决 nginx 的权限问题( Permission denied)
  8. 项目中遇到的问题及解决办法
  9. [tensorflow]tensorflow2.0的优化理论
  10. 达观杯文本智能处理(6)
  11. php点击按钮变文字,点击按钮文字变成input框,点击保存变成文字的实现代码
  12. 浏览器cookie被禁掉,该如何去解决
  13. Dockerfile Registry WebUI 之 docker-registry-frontend 高级应用
  14. 鲁大师2021年度手机报告:5大“最强”手机揭晓,还有最流畅UI
  15. c语言中mul的用法,MUL指令(无符号数的乘法指令)
  16. XSS讲解上(web安全入门10)
  17. 大学计算机专业实习报告
  18. 【思维与逻辑】有1000瓶药水,但其中有一瓶毒药水,需要多少只小白鼠?
  19. C++编译调试问题(一)
  20. python 绘制科赫雪花

热门文章

  1. 大脑小胶质细胞“隐藏技能”被发现
  2. mysql数据库安装文件多大_MySQL数据库的安装
  3. iPics2Go: iPhone变身扫描仪
  4. 充电电池的充放电电流-0.2C、1C、2C的含义
  5. 新能源汽车——动力电池
  6. java怎么比较两个list是否相同?
  7. EBS中如何实现简单的日记账导入
  8. UltraISO(软碟通)制作U盘启动盘完整教程
  9. HUD1873看病要排队
  10. 笑对人生,坐看云起云落