并行编程模型之Actor/CSP/PGAS

Actor

1.背景

  处理并发问题就是如何保证共享数据的一致性和正确性,一般来说有两种策略用来在并发线程中进行通信:共享数据和消息传递。
  熟悉c和java并发编程的都会比较熟悉共享数据的策略,比如java程序员就会常用到java.util.concurrent包中同步、锁相关的数据结构。使用共享数据方式的并发编程面临的最大的一个问题就是数据条件竞争(data race)。共同思路就是将数据喂给线程,数据是被动的,自身不复杂,没有自身业务逻辑要求。适合大数据处理或互联网网站应用等等。但是如果数据自身要求有严格的一致性,也就是事务机制,数据就不能被动被加工,要让数据自己有行为能力保护实现自己的一致性。处理各种锁的问题是让人十分头痛的一件事。
  要让数据自己有行为维护自己的一致性,才能真正安全实现真正的事务。所以,数据+行为=对象。
  但是这还不够,因为即使数据能够自己有行为,发起行为的可能还是线程,本质上还是无法避免多线程环境下的数据共享问题。所以出现了消息机制,每个数据只接受消息,真正何时何种方式执行行为,完全由数据决定。和共享数据方式相比,消息传递机制最大的优点就是不会产生数据竞争状态(data race)。
  实现消息传递有两种常见的类型:基于channel的消息传递和基于Actor的消息传递。

2. 简介

  Actor这个模型由Carl Hewitt在1973年提出,Gul Agha在1986年发表技术报告“Actors: A Model of Concurrent Computation in Distributed Systems”,至今已有不少年头了,在Erlang语言中得到广泛支持和应用。在计算机科学中,它是一个并行计算的数学模型,最初为由大量独立的微处理器组成的高并行计算机所开发。
  Actor模型把actors当做通用的并行计算原语:一个actor对接收到的消息做出响应,进行本地决策,可以创建更多的actor,或者发送更多的消息;同时准备接收下一条消息。
  在Actor理论中,一切都被认为是actor,这和面向对象语言里一切都被看成对象很类似。但包括面向对象语言在内的软件通常是顺序执行的,而Actor模型本质上则是并发的。
  一个Actor指的是一个最基本的计算单元。它能接收一个消息并且基于其执行计算。

Actor模型=数据+行为+消息

  Actors一大重要特征在于actors之间相互隔离,它们并不互相共享内存。这点区别于上述的对象。也就是说,一个actor能维持一个私有的状态,并且这个状态不可能被另一个actor所改变。

  Actor与Actor之间只能通过消息通信。Actor模型内部的状态由自己的行为维护,外部线程不能直接调用对象的行为,必须通过消息才能激发行为,这样就保证Actor内部数据只有被自己修改。这就解释了为什么Actor模型是一种处理并发问题的解决方案。

  Actor由状态(state)、行为(Behavior)和邮箱(mailBox)三部分组成

  • 状态(state):Actor中的状态指的是Actor对象的变量信息,状态由Actor自己管理,避免了并发环境下的锁和内存原子性等问题
  • 行为(Behavior):行为指定的是Actor中计算逻辑,通过Actor接收到消息来改变Actor的状态
  • 邮箱(mailBox):邮箱是Actor和Actor之间的通信桥梁,邮箱内部通过FIFO消息队列来存储发送方Actor消息,接受方Actor从邮箱队列中获取消息
    Actor的基础就是消息传递

Actor有以下几个特点:

  1. 每个Actor都有对应一个邮箱。
  2. 消息异步地传送到actor。
    所以当actor正在处理消息时,新来的消息应该存储到别的地方。Mailbox就是这些消息存储的地方。
  3. 每个Actor是串行处理邮箱中的消息的。
    也就是说其它actors发送了三条消息给一个actor,这个actor只能一次处理一条。所以如果你要并行处理3条消息,你需要把这条消息发给3个actors。
  4. Actor中的消息是不可变的

3.actor组成

  模型中一个Actor是一个基本的计算单元。它接受消息然后基于接到的消息做一些计算。和面向对象编程有些类似,一个对象被调用(接收到一个消息),基于调用方法(接受到的一个消息)做处理。区别是actor之间是完全隔离的,不共用内存区域。actor的私有状态不会被另外一个actor直接改变。
  actor作为群体存在,单一的actor不是actor模式。在actor模型中,actor是唯一组成部分,actor带有地址以便互相发送消息。
  actor按次序处理消息,比如你发送三个消息给一个actor,它们不会被并发处理。如果你想让这三个消息得到并发处理,你需要创建3个actor,然后分别发送给它们。
  接受到的异步消息存在于actor内部的一个队列中,我们可以把它形象化的叫做邮箱(mailbox)。
在这里插入图片描述

Actor

  Actor的概念来自于Erlang,在AKKA中可以认为一个Actor就是一个容器,用来存储状态、行为、邮箱Mailbox、子Actor、Supervisor策略。Actor之间并不直接通信,而是通过邮件Mail来互通有无。Actor模型的本质就是消息传递,作为一种计算实体,Actor与原子类似。参与者是一个运算实体,回应接收到的消息,同时并行的发送有限数量的消息给其他参与者、创建有限数量的新参与者、指定接收到下一个消息时的行为。
  Actor模型推崇的哲学是”一切皆是参与者“,与面向对象编程的”一切皆是对象“类似,但面向对象编程通常是顺序执行的,而Actor模型则是并行执行的。一个Actor指的是一个最基本的计算单元,能够接受一个消息并基于它执行计算。这个理念也很类似面向对象语言中:一个对象接收一个消息(方法调用),然后根据接收的消息做事儿(调用了哪个方法)。Actors一大重大特征在于actors之间相互隔离,它们并不相互共享内存。这点区别于上述的对象,也就是说,一个actor能维持一个私有的状态,并且这个状态不可能被另一个actor所改变。
  在Actor模型中主角是actor,类似一种worker。Actor彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的。在Actor模型中一切都是Actor,所有逻辑或模块都可以看成是Actor,通过不同Actor之间的消息传递实现模块之间的通信和交互。

Mailbox邮箱

  在Actor模型中每个actor都有自己的地址,所以他们才能相互发送消息。需要指明的一点是,尽管多个actors同时运行,但是一个actor只能顺序地处理消息。也就是说其它actor发送多条消息给一个actor时,这个actor只能一次处理一条。如果需要并行的处理多条消息时,需要将消息发送给多个actor。
  消息是异步的传送到actor的,所以当actor正在处理消息时,新来的消息应该存储到别的地方,也就是mailbox消息存储的地方。
在这里插入图片描述

behavior行为

当一个actor接收到消息后,它可能做三件事:

  1. 创建更多的actor
  2. 发送消息给其他的actor
  3. 决定收到下一条消息应该做什么
      前两个点非常好理解,我们来说说第三条。前面也提到过,actor会维护私有的状态,第三条意味着:接收到下一条消息后,私有状态应该是怎样的(这就跟状态机的概念非常类似了),或者更简单点,应该如何修改状态。
      我们来想象一下,如果一个actor的行为类似于计算器,它的初始状态为0. 当它接收到"add(1)"的消息后,它将会在下次接收到消息后,将状态设为1;而不是在本次接收到消息后修改状态。

4.优势

  对并发模型进行了更高的抽象
  异步、非阻塞、高性能的事件驱动编程模型
  轻量级事件处理(1GB内存可容纳百万级别个Actor)

(1)简化并发编程:
  并发导致最大的问题就是对共享数据的操作,我们在面对并发问题时多采用的是用锁去保证共享数据的一致性,但这同样也会带来其他相关问题,比如要去考虑锁的粒度(对方法,程序块等),锁的形式(读锁,写锁等)等问题,这些问题对无疑给程序员在编程上提高了复杂性。但使用Actor就不导致这些问题,首先Actor的消息特性就觉得了在与Actor通信上不会有共享数据的困扰,另外在Actor内部是串行处理消息的,同样不会对Actor内的数据造成污染,用Actor编写并发程序无疑大大降低了编码的复杂度。
(2)提升程序性能:
  我们之前说过既然用单线程处理,那如何保证程序的性能?首先Actor是非常轻量级的,你可以再程序中创建许多个Actor,而且Actor是异步的,那么如何利用它的这个特性呢,我们要做的就是把相应的并发事件尽可能的分割成一个个小的事件,让每个Actor去处理相应的小事件,充分去利用它异步的特点,来提升程序的性能。

无锁

  在使用Java/C# 等语言进行并发编程时需要特别的关注锁和内存原子性等一系列线程问题,而Actor模型内部的状态由它自己维护即它内部数据只能由它自己修改(通过消息传递来进行状态修改),所以使用Actors模型进行并发编程可以很好地避免这些问题。Actor内部是以单线程的模式来执行的,类似于redis,所以Actor完全可以实现分布式锁类似的应用。

异步

  每个Actor都有一个专用的MailBox来接收消息,这也是Actor实现异步的基础。当一个Actor实例向另外一个Actor发消息的时候,并非直接调用Actor的方法,而是把消息传递到对应的MailBox里,就好像邮递员,并不是把邮件直接送到收信人手里,而是放进每家的邮箱,这样邮递员就可以快速的进行下一项工作。所以在Actor系统里,Actor发送一条消息是非常快的。

隔离

  每个Actor的实例都维护这自己的状态,与其他Actor实例处于物理隔离状态,并非像 多线程+锁 模式那样基于共享数据。Actor通过消息的模式与其他Actor进行通信,与OO式的消息传递方式不同,Actor之间消息的传递是真正物理上的消息传递。

容错

  Erlang里面有一个非常有名的哲学"let it crash". 指的是你不应该进行防守式编程,尝试预测到所有可能的问题,然后找到一种方法来处理他们,因为你不可能考虑到所有的错误点。
  Erlang只是简单的让程序crash掉,但是要让这段关键的代码由某个模块负责监控,它的唯一职责是知道当crash发生时该怎么做(比如说重启这段应用来回到一个稳定的状态),这就是Actor模型负责干的事。
  每段代码都运行在进程(process,也就是actor概念)中,进程是完全隔离的,这意味着一个进程中的状态不会影响到其他的进程。同时,上面也提到了,需要有负责监控的模块,这就是另外一个进程(一切都是actor,包括进程)。当被监控的进程crash掉了,将会通知这个负责监控的进程,然后处理剩下的任务。
  通过上面的这种方式,我们就能构建一个可以自愈(self heal)的系统,当一个actor进入到异常状态,发生了crash,监控的actor能够去负责处理,然后回到一个一致的状态(例如重置为最初的状态)。

分布式

  每个Actor实例的位置透明,无论Actor地址是在本地还是在远程机器上对于代码来说都是一样的。每个Actor的实例非常小,最多几百字节,所以单机几十万的Actor的实例很轻松。如果你写过golang代码,就会发现其实Actor在重量级上很像Goroutine。由于位置透明性,所以Actor系统可以随意的横向扩展来应对并发,对于调用者来说,调用的Actor的位置就在本地,当然这也得益于Actor系统强大的路由系统。
  每个Actor实例都有自己的生命周期,就像C# java 中的GC机制一样,对于需要淘汰的Actor,系统会销毁然后释放内存等资源来保证系统的持续性。其实在Actor系统中,Actor的销毁完全可以手动干预,或者做到系统自动化销毁。

5.劣势

  1. 由于同一类型的Actor对象是分散在多个宿主之中,所以取多个Actor的集合是个软肋。比如在电商系统中,商品作为一类Actor,查询一个商品的列表在多数情况下经过以下过程:首先根据查询条件筛选出一系列商品id,根据商品id分别取商品Actor列表(很可能会产生一个商品搜索的服务,无论是用es或者其他搜索引擎)。如果量非常大的话,有产生网络风暴的危险(虽然几率非常小)。在实时性要求不是太高的情况下,其实也可以独立出来商品Actor的列表,利用MQ接收商品信息修改的信号来处理数据一致性的问题。
  2. 在很多情况下基于Actor模型的分布式系统,缓存很有可能是进程内缓存,也就是说每个Actor其实都在进程内保存了自己的状态信息,业内通常把这种服务成为有状态服务。但是每个Actor又有自己的生命周期,会产生问题吗?呵呵,也许吧。想想一下,还是拿商品作为例子, 如果环境是非Actor并发模型,商品的缓存可以利用LRU策略来淘汰非活跃的商品缓存,来保证内存不会使用过量,如果是基于Actor模型的进程内缓存呢,每个actor其实就是缓存本身,就不那么容易利用LRU策略来保证内存使用量了,因为Actor的活跃状态对于你来说是未知的。
  3. 分布式事物问题,其实这是所有分布式模型都面临的问题,非由于Actor而存在。还是以商品Actor为例,添加一个商品的时候,商品Actor和统计商品的Actor(很多情况下确实被设计为两类Actor服务)需要保证事物的完整性,数据的一致性。在很多的情况下可以牺牲实时一致性用最终一致性来保证。
  4. 每个Actor的mailBox有可能会出现堆积或者满的情况,当这种情况发生,新消息的处理方式是被抛弃还是等待呢,所以当设计一个Actor系统的时候mailBox的设计需要注意。

6.实践

素数计算

需求:使用多线程找出1000000以内素数个数
在这里插入图片描述
  传统方式通过锁/同步的方式实现并发,每次同步获取当前值并让一个线程去判断值是否为素数,若是的话则通过同步方式对计数器加一。
在这里插入图片描述
  使用Actor模型方式会将此过程拆分成多个模块,即拆分成多个Actor。每个Actor负责不同部分,并通过消息传递让多个Actor协同工作。

CSP

1.简介

  CSP(Communicating Sequential Process)并发模型最初于Tony Hoare的1977年的论文中被描述,影响了许多编程语言的设计。
CSP的核心思想是多个线程之间通过Channel来通信(对应到golang中的chan结构),有点像是管道的概念。(Pipe)
在这里插入图片描述

2.CSP与go语言

  CSP本身是一套数学语言,用来描述Process以及Process之间的通讯的。当一个Process使用数学语言描述了出来,我们就可以对其进行数学推导简化和分析,比如研究会不会发生死锁和进入发散无序状态等等。
  既然CSP可以用来分析多线程的,那么计算机语言也提供了对应的支持。
  其中支持得最好的就是Golang。其中协程就是CSP中的Process,Channel就负责实现CSP中的Process通讯。在程序逻辑层面代码还是同步的,碰到Process通讯的时候还是会把当前的Process阻塞住的,但是在Golang协程本身的阻塞并不会阻塞住线程,所以还是可以理解成一种异步的实现方式。
  Go是一门号称从语言层面支持并发的编程语言,支持并发也是Go非常重要的特性之一。
  Go支持协程,协程可以类比Java中的线程,解决并发问题的难点在于线程(协程)之间的协作。Go提供了两种方案:

  1. 支持协程之间以共享内存的方式通信,Go提供了管程和原子类来对协程进行同步控制,该方案与Java类似;
  2. 支持协程之间以消息传递的方式通信,本质上是要避免共享,该方案是基于CSP模型实现的,Go推荐该方案。

2.1 组成

channel
  channel一种个安全的双端队列,任何任务只要持有channel的引用,就可以向一端加入消息,也可以向一端删除消息,消息的消费者和生产者不清楚对方是谁。
  channel分为无缓冲和有缓冲,无缓冲会同步阻塞,即每次生产消息都会阻塞到消费者将消息消费;有缓冲的不会立刻阻塞。
  关闭channel:向关闭的channel读数据会是nil,向关闭的channel写数据会被弃用。
  channel缓冲区已满的处理策略,默认策略是阻塞写消息,直至写成功

  • dropping-buffer:缓冲满后忽略最迟的消息
  • sliding-buffer:缓冲满后弃用最早的消息

  为什么不支持容量自动扩展的channel:有限的资源总有会遇到资源耗尽的时候,如果因为各种外来内在原因而出现消息堆积的场景,在重重代码种会变成非常难排除的bug,所以最好的处理方案,就是在任何场景使用缓冲的channel,必须考虑缓冲溢出如何处理。
go 块
  线程在其他语言比如Java的使用方案,首先可以单独创建线程处理任务,因为线程重复创建销毁会带来很多的性能开销,所以首选使用线程池技术,但是线程池技术在线程通信的时候,还有其他缺陷比如,如果线程被阻塞,这个线程将被无限期占用,这个在高并发的场景下,削弱了线程池的优势。
  如果要补全这个缺陷,也有其他的解决方案比如事件驱动,仅仅当被通知IO准备好时,线程才会去处理这个IO请求,这个方案可以实现但是会限制代码的编写风格,使用复杂,可读性非常差。
  go块从根本提供了解决方案,在底层通信代码中,将底层串行通信的代码重写为事件驱动的代码(channel?),而上层业务代码无需考虑IO切换,就能实现灵活的线程阻塞时的切换。

  • 控制反转:go块代码是一个状态机,当读写channel时,会将go块状态变为暂停,主动让出线程,等待go块继续运行的时候,会请求已有或新线程,然后状态转换后继续运行。
  • 意义:不用担心资源而随意创建并发任务。

2.2Goroutine调度器

在这里插入图片描述
  M代表系统线程,P代表处理器(核),G代表Goroutine。Go实现了M:N的调度,也就是说线程和Goroutine之间是多对多的关系。这点在许多GreenThread/Coroutine的调度器并没有实现。比如Java1.1版本之前的线程其实是GreenThread(这个词就来源于Java),但由于没实现多对多的调度,也就是没有真正实现并行,发挥不了多核的优势,所以后来改成基于系统内核的Thread实现了。
  某个系统线程如果被阻塞,排列在该线程上的Goroutine会被迁移。当然还有其他机制,比如M空闲了,如果全局队列没有任务,可能会从其他M偷任务执行,相当于一种rebalance机制。
  系统启动时,会启动一个独立的后台线程(不在Goroutine的调度线程池里),启动netpoll的轮询。当有Goroutine发起网络请求时,网络库会将fd(文件描述符)和pollDesc(用于描述netpoll的结构体,包含因为读/写这个fd而阻塞的Goroutine)关联起来,然后调用runtime.gopark方法,挂起当前的Goroutine。当后台的netpoll轮询获取到epoll(linux环境下)的event,会将event中的pollDesc取出来,找到关联的阻塞Goroutine,并进行恢复。

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

二者的格言都是:

Don’t communicate by sharing memory, share memory by communicating

通过消息通信的机制来避免竞态条件,但具体的抽象和实现上有些差异。
在这里插入图片描述

  • CSP模型里消息和Channel是主体,处理器是匿名的。也就是说发送方需要关心自己的消息类型以及应该写到哪个Channel,但不需要关心谁消费了它,以及有多少个消费者。Channel一般都是类型绑定的,一个Channel只写同一种类型的消息,所以CSP需要支持alt/select机制,同时监听多个Channel。Channel是同步的模式(Golang的Channel支持buffer,支持一定数量的异步),背后的逻辑是发送方非常关心消息是否被处理,CSP要保证每个消息都被正常处理了,没被处理就阻塞着。
    在这里插入图片描述
  • Actor模型里Actor是主体,Mailbox(类似于CSP的Channel)是透明的。也就是说它假定发送方会关心消息发给谁消费了,但不关心消息类型以及通道。所以Mailbox是异步模式,发送者不能假定发送的消息一定被收到和处理。Actor模型必须支持强大的模式匹配机制,因为无论什么类型的消息都会通过同一个通道发送过来,需要通过模式匹配机制做分发。它背后的逻辑是现实世界本来就是异步的,不确定(non-deterministic)的,所以程序也要适应面对不确定的机制编程。自从有了并行之后,原来的确定编程思维模式已经受到了挑战,而Actor直接在模式中蕴含了这点。
    在这里插入图片描述
      从这样看来,CSP的模式比较适合Boss-Worker模式的任务分发机制,它的侵入性没那么强,可以在现有的系统中通过CSP解决某个具体的问题。它并不试图解决通信的超时容错问题,这个还是需要发起方进行处理。同时由于Channel是显式的,虽然可以通过netchan(原来Go提供的netchan机制由于过于复杂,被废弃,在讨论新的netchan)实现远程Channel,但很难做到对使用方透明。而Actor则是一种全新的抽象,使用Actor要面临整个应用架构机制和思维方式的变更。它试图要解决的问题要更广一些,比如容错,比如分布式。但Actor的问题在于以当前的调度效率,哪怕是用Goroutine这样的机制,也很难达到直接方法调用的效率。当前要像OO的『一切皆对象』一样实现一个『一切皆Actor』的语言,效率上肯定有问题。所以折中的方式是在OO的基础上,将系统的某个层面的组件抽象为Actor。

PGAS

1.简介

数据并行模型
  通常也被称为“全局地址空间分区”(Partitioned Global Address Space (PGAS))模型。具有如下特点:

地址空间被认为是全局的
  大多数的并行工作聚焦于在数据集上的操作。数据集通常被组织成为常用的结构,例如数组,数立方等。
  一系列任务在同一块数据结构上操作,但是每个任务却操作在该数据结构的不同分区上。
  每个任务在数据结构的不同分区上执行相同的操作,例如,“给每个数组元素加上4”。
在这里插入图片描述
  在共享内存的架构下,所有的任务通过全局内存方式来对数据进行存取;在分布式内存架构下,根据任务分配,全局数据结构在物理或者逻辑上被进行分割。

2.实现

  目前,基于数据并行/PGAS模型,有如下几个相对有名的实现:

  • Coarray Fortran: 为了支持SPMD并行编程而在Fortran 95上做的一个小的扩展,是编译器相关的,更多信息可以参见:
    https://en.wikipedia.org/wiki/Coarray_Fortran
  • Unified Parallel C (UPC): 为了支持SPMD并行编程而在C语言基础上做的扩展,也是编译器相关的,更多信息可以参见:http://upc.lbl.gov/
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值