admin管理员组

文章数量:829155

浅谈Actor并发模型

目录

0x00    Actor出现的背景

0x01    Actor如何通过消息传递避免数据竞争?

0x02    Actor到底是什么?

0x03    Actor特性

0x031    容错

0x032    分布式与位置透明性

0x04    Actor使用场景

0x05    缺点

0x051弱隔离性

0x052其他问题


0x00    Actor出现的背景

大约在 2003 年左右,计算机的核心特性经历了一个重要的变化,处理器的速度达到了一个顶点。在接下来的近十五年里,时钟速度是呈线性增长的,而不是像以往那样以指数级的速度增长。但是,用户的需求在持续增长,计算机领域必须要找到某种方式来应对,多核处理器应运而生。计算处理变成了“团队协作”,效率的提升通过多个核心的通信来实现,而不是传统的时钟速度的提升。而针对这种情况,要使我们的程序运行效率提高,那么也应该从并发方面入手。

处理并发问题一贯的思路是如何保证共享数据的一致性和正确性(隔离性)。一般而言,有两种策略用来在并发线程中进行通信:

  1. 共享数据

  2. 消息传递

使用共享数据的并发编程面临的最大问题是数据条件竞争,处理各种锁的问题是让人十分头疼的。

  1. 死锁:锁的设计不当会造成死锁;
  2. 效率低:在高度竞争的阶段,很有可能出现很长的线程队列,他们都在等待递减计数器。但使用队列的方式的问题在于可能造成众多阻塞线程,也就是每个线程都在等待轮到它们去执行一个序列化的操作。所以,应用设计者一不小心,内在的复杂性就有可能将多核多线程的应用变成单线程的应用,或者导致工作线程之间存在高度竞争。

和共享数据方式相比,消息传递机制最大的优势在于不会产生数据竞争状态。而实现消息传递有两种常见类型:

  1. 基于channel的消息传递

  2. 基于Actor的消息传递

0x01    Actor如何通过消息传递避免数据竞争?

想象我们在抢火车票,有两个线程并发地调整计数器,该计数器目前的值是 5。线程一想要将计数器的值递减 3,而线程二想要将计数器的值递减 4。它们都会检查当前计数器的值,并且会断定计数器的值大于要递减的数量。然后,它们都会继续运行并递减计数器的值。最后的结果就是 5 - 4 - 3 = -2。这样的结果会造成货品的过度分配,违反了特定的业务规则。

现在,我们将基于线程的实现替换为 Actor。当然,Actor 也要在线程中运行,但是 Actor 只有在有事情可做的时候才会使用线程。在我们的计数器场景中,请求者代表了 Customer Actor。门票的数量现在由 Actor 来维护,它持有当前计数器的状态。Customer Actor 和 Tickets Actor 在空闲时(也就是没有消息要处理)都不会持有线程。

要初始购买操作,Customer Actor 需要发送一条 buy 消息给一个 Tickets Actor。在这样的 buy 消息中包含了要购买的数量。当 Tickets Actor 接收到 buy 消息时,它会校验购买数量不超过当前剩余的数量。如果购买请求是合法的,数量就会递减,Tickets Actor 会发送一条信息给 Customer Actor,表明订单被成功接受。如果购买数量超出了剩余数量的话,Tickets Actor 将会发送给 Customer Actor 一条消息,表明订单被拒绝了。Actor 模型本身确保处理是按照同步的方式进行的。

我们分三步展示actor之间的交互:

  1. Customer Actor 发送 buy 消息

  2. Tickets Actor 处理消息

  3. Tickets Actor 拒绝购买请求

1.Customer Actor 发送 buy 消息: Customer Actor,它们各自发送 buy 消息给 Tickets Actor。这些 buy 消息会在 Tickets Actor 的收件箱(mailbox)中排队。发送一条消息并未将“执行线程”从发送者转移到目标。一个actor可以发送一条消息并继续无阻塞地运行。因此,在同样的时间内,它可以完成更多任务。

图1:Customer Actor 发送 buy 消息

2.Tickets Actor 处理消息:如下展示的是请求购买五张门票的第一条消息。

图 2:Tickets Actor 处理消息

当一个Tickets  Actor收到一条消息时,Tickets  Actor将这条消息添加到队列尾部,如果Tickets  Actor没有被调度执行,它将被标记为ready。一个调度器获取这个Tickets  Actor并开始执行它:Tickets  Actor在队列头部取出一条消息。

随后,Tickets Actor 检查购买数量没有超出剩余门票的数量。在当前的情况下,门票数量是 15,因此购买请求能够接受,剩余门票数量会递减,Tickets Actor 还会发送一条消息给发出请求的 Customer Actor,表明门票购买成功。

图 3:Tickets Actor 处理消息队列

Tickets Actor 会处理其收件箱中的每条消息。需要注意,这里没有复杂的线程或锁。这是一个多线程的处理过程,但是 Actor 系统会管理线程的使用和分配。

在这里Actor区别于多线程并发模型的是,多线程并发模型改变了actor并对内部状态,而actor独立处理收到的消息,并且它们一个一个地响应连续到来的消息。虽然每个actor连续地处理发给它的消息,不同的actors之间并发地工作,所以一个actor系统可以同时处理多条消息。因为每个actor中同时最多处理一个消息,所以Actor模型无需使用锁。

3.Tickets Actor 拒绝购买请求:当请求的数量超过剩余值时,Tickets Actor 会如何进行处理。这里所展现的是当我们请求两张门票,但是仅剩一张门票时的情况。Tickets Actor 会拒绝这个购买请求并向发起请求的 Customer Actor 发送一条“sold out”的消息。

图 4:Tickets Actor 拒绝购买请求

当然,在线程方面有一定经验的开发人员会知道,可划分为两个阶段的行为检查和门票数量递减能够通过同步的操作序列来完成。以在 Java 中为例,我们可以使用同步的方法或语句来实现。但是,基于 Actor 的实现不仅在每个 Actor 中提供了自然的操作同步,而且还能避免大量的线程积压,防止这些线程等待轮到它们执行同步代码区域。在门票样例中,每个 Customer Actor 会等待响应,此时不会持有线程。这样所形成的结果就是基于 Actor 的方案更容易实现,并且会明显降低系统资源的占用。

0x02    Actor到底是什么?

说了这么多,归根到底就是两点:

  1. actor通过消息传递的方式与外界通信。消息传递是异步的。每个actor都有一个邮箱,该邮箱接收并缓存其他actor发过来的消息,actor一次只能同步处理一个消息,处理消息过程中,除了可以接收消息,不能做任何其他操作。
  2. Actor模型的另一个好处就是可以消除共享状态,因为它每次只能处理一条消息,所以actor内部可以安全的处理状态,而不用考虑锁机制。消息传输和封装虽然多个actor可以同时运行,但它们并不共享状态,而且在单个actor中所有事件都是串行执行的。

下面这张图展示了一个简单的 Actor 模型系统:

Actor是由状态(state)、行为(behavior)、邮箱(mailbox)三者组成的:

  • 状态(state):状态是指actor对象的变量信息,状态由actor自身管理,避免并发环境下的锁和内存原子性等问题。
  • 行为(behavior):行为指定的是actor中计算逻辑,通过actor接收到的消息来改变actor的状态。
  • 邮箱(mailbox):邮箱是actor之间的通信桥梁,邮箱内部通过FIFO消息队列来存储发送发消息,而接收方则从邮箱中获取消息。

Actor模型描述了一组为避免并发编程的公理:

  • 所有的Actor状态是本地的,外部是无法访问的。
  • Actor必须通过消息传递进行通信
  • 一个Actor可以响应消息、退出新Actor、改变内部状态、将消息发送到一个或多个Actor。
  • Actor可能会堵塞自己但Actor不应该堵塞自己运行的线程

0x03    Actor特性

0x031    容错

  Actor模型通过监督机制提供容错。这跟java中的throw exception有点类似,都是把处理响应错误的责任交给出错对象以外的实体。但在java中如果一个程序或者线程抛出了一个异常,你敢放心的恢复对应的程序或线程吗?你确保恢复之后还能正常的运行吗,毕竟需要很多资源需要重新创建。但Actor模型可以!

如上图所示actor之间是有层级关系的,子actor如果出现了异常会抛给父actor,父actor会根据情况重新构建子actor,子actor从出现异常,到恢复之后正常运行,这段时间内的所有消息都不会丢失,等恢复之后又可以处理下一个消息。也就是说如果一个actor抛出了异常,除了导致发生异常的消息外,任何消息都不会丢失。

0x032    分布式与位置透明性

Actor模型中一个很重要的概念就是actor地址,因为其他actor需要通过这个地址与actor进行通信。akka考虑到分布式的网络环境,对actor地址进行了抽象,屏蔽了本地地址和远程地址的差异,对于开发者来说基本上是透明的。

0x04    Actor使用场景

  1. 对高并发有严格要求的同时又要维护某种状态
  2. 构建有限状态机,如果只是处理一个有限状态机,使用一个actor即可,如果是多个有限状态机,而且还要彼此交互,更应该选择actor模式
  3. 需要高并发,同时也需要很小心地管理并发
    eg:需要确保特定的一组操作可以与系统中的某些操作并发运行,但不能与系统中其他操作并发运行

0x05    缺点

0x051弱隔离性

Actor模型针对一致性要求比较强的场景比较乏力。以银行转账为例,假设有两个用户,现在用户A向用户B转账100元。用户 A 和 用户 B 明显是两个 Actor ,但我们同时还需要一个可以控制用户A Actor 和用户B Actor 的 Actor ,我们称之为 转账管家 Actor,流程图如下。

可以看到,当一个转账需求过来的时候,Actor 管家会先向 用户A Actor 发送扣款 100 元的信息,接受到扣款成功消息后再发送消息给用户B Actor,发送让其增加 100 元的消息。

但是!在用户A Actor 扣款期间,用户B Actor 是不受限制的,此时对用户B Actor 进行操作是合法的!针对这种情况单纯的Actor模型就显得比较乏力了,需要加入其他机制以保证一致性。

0x052其他问题

尽管使用actor模型的程序比使用线程与锁模型的程序更容易debug,但actor模型仍会碰到死锁这一类的共性问题,也会碰到一些actor模型独有的问题(例如信箱溢出)。

类似于线程与锁模型, actor模型对并行也没有提供直接支持。需要通过并发的技术来构造并行的方案,这样就会引入不确定性。而且,由于多个actor并不共享状态,仅通过消息传递来进行交流,所以不太适合实施细粒度的并行。

参考:

.html

/

.html

.html

本文标签: 浅谈Actor并发模型