0%

akka(二)——Actor模型

Actor模型

Actor——Actor模型原理的通俗理解 (转) - 会飞的斧头 - 博客园 (cnblogs.com)

actor模型的优点:

简化并发编程,提升程序性能

Akka是一个开发并发、容错和可伸缩应用的框架。它是Actor Model的一个实现,和Erlang的并发模型很像。在Actor模型中,所有的实体被认为是独立的actors。actors和其他actors通过发送异步消息通信。Actor模型的强大来自于异步。它也可以显式等待响应,这使得可以执行同步操作。但是,强烈不建议同步消息,因为它们限制了系统的伸缩性。每个actor有一个邮箱(mailbox),它收到的消息存储在里面。另外,每一个actor维护自身单独的状态。

一个Actors网络如下所示:

img

每个actor是一个单一的线程,它不断地从其邮箱中poll(拉取)消息,并且连续不断地处理。对于已经处理过的消息的结果,actor可以改变它自身的内部状态或者发送一个新消息或者孵化一个新的actor。尽管单个的actor是自然有序的,但一个包含若干个actor的系统却是高度并发的并且极具扩展性的。因为那些处理线程是所有actor之间共享的。这也是我们为什么不该在actor线程里调用可能导致阻塞的“调用”。因为这样的调用可能会阻塞该线程使得他们无法替其他actor处理消息。

如何满足现代分布式系统的需求?

使用 Actor 允许我们:

  • 在不使用锁的情况下强制封装。
  • 利用协同实体对信号作出反应、改变状态、相互发送信号的模型来驱动整个应用程序向前发展。

消息传递的使用避免了锁和阻塞

Actor 不调用方法,而是互相发送消息。发送消息不会将线程的执行权从发送方传输到目标方。Actor 可以发送一条消息并继续其他操作,而不是阻塞。因此,它可以在相同的时间内完成更多的工作。

对于对象,当一个方法返回时,它释放对其执行线程的控制。在这方面,Actor 的行为非常类似于对象,它们对消息作出反应,并在完成当前消息的处理后执行返回。通过这种方式,Actor 实际上实现了我们设想中对象的执行方式:

actors interact with each other by sending messages

传递消息和调用方法之间的一个重要区别是消息没有返回值。

通过发送消息,Actor 将工作委托给另一个 Actor。正如我们在「调用栈的假象」中看到的,如果它期望返回值,那么发送 Actor 要么阻塞,要么在同一线程上执行另一个 Actor 的工作。相反,接收 Actor 在回复消息中传递结果。

Actor的消息处理过程

许多actors同时运行,但是一个actor只能顺序地处理消息。

如果其它actors发送了三条消息给一个actor,这个actor只能一次处理一条。所以如果你要并行处理3条消息,你需要把这条消息发给3个actors。

由于每个 Actor 最多只能同时处理一条消息,因此可以不同步地保留 Actor 的不变量。这是自动发生的,不使用锁:

messages do not invalidate invariants as they are processed sequentially

总之,当 Actor 收到消息时会发生以下情况:

  1. Actor 将消息添加到队列的末尾。
  2. 如果 Actor 没有执行计划,则将其标记为准备执行。
  3. 一个(隐藏的)调度程序实体获取 Actor 并开始执行它。
  4. Actor 从队列前面选择消息。
  5. Actor 修改内部状态,向其他 Actor 发送消息。
  6. Actor 没有预约(unscheduled)。

为了完成上面的行为,Actors 有:

  • 一个邮箱(消息结束的队列)
    • 消息异步地传送到actor,所以当actor正在处理消息时,新来的消息应该存储到别的地方。Mailbox就是这些消息存储的地方。
  • 一个行为(Actor 的状态、内部变量等)
    • 标志下条消息来时actor自身的状态
  • 消息(表示信号的数据片段,类似于方法调用及其参数)。
  • 一个执行环境(一种机制,它让具有消息的 Actor 对其消息处理代码作出反应并调用它们)。
  • 一个地址。

消息进入 Actor 邮箱。Actor 的行为描述了 Actor 如何响应消息(如发送更多消息和/或更改状态)。执行环境协调线程池以完全透明地驱动所有这些操作。

这是一个非常简单的模型,它解决了前面列举的问题:

  • 通过将执行与信号分离(方法调用转换执行权,消息传递不这样做),可以保留封装。
  • 不需要锁。修改 Actor 的内部状态只能通过消息来实现,一次处理一条消息,在试图保持不变时消除竞争。
  • 任何地方都没有使用锁,发送者也不会阻塞。数百万个 Actor 可以有效地安排在十几个线程上,从而充分发挥现代 CPU 的潜力。任务委托是 Actor 的天然执行方式。
  • Actor 的状态是本地的而不是共享的,更改和数据通过消息传播,消息是映射到现代内存架构的实际工作方式。在许多情况下,这意味着只传输包含消息中数据的缓存线,同时将本地状态和数据缓存在原始核心上。相同的模型可以完全映射到远程通信中,其中状态保存在机器的 RAM 中,更改/数据作为数据包在网络上传播。

Actor系统

一个Actor系统包含了所有存活的actors。它提供的共享服务包括调度、配置和日志等。Actor系统同时包含一个线程池,所有actor从这里获取线程。

多个Actor系统可以在一台机器上共存。如果一个Actor系统通过RemoteActorRefProvider启动,它就可以被其他机器上的Actor系统发现。Actor系统能够自动识别消息是发送给本地机器还是远程机器的Actor系统。在本地通信的情况下,消息通过共享存储器高效的传输。在远程通信的情况下,消息通过网络栈发送。

所有Actors都是继承来组织的。每个新创建的actor将其创建的actor视作父actor。继承被用来监督。每个父actor对自己的子actor负责监督。如果在一个子actor发生错误,父actor将会收到通知。如果这个父actor可以解决这个问题,它就重新启动这个子actor。如果这个错误父actor无法处理,它可以把这个错误传递给自己的父actor。

Actor处理错误情况

由于我们不再拥有在相互发送消息的 Actor 之间共享的调用栈,因此我们需要以不同的方式处理错误情况。我们需要考虑两种错误:

  • 第一种情况是,由于任务中的错误(通常是一些验证问题,如不存在的用户 ID),目标 Actor 上的委派任务失败。在这种情况下,由目标 Actor 封装的服务是完整的,只有任务本身是错误的。服务 Actor 应该用一条消息回复发送者,并显示错误情况。这里没有什么特别的,错误是域的一部分,因此错误也是普通消息

  • 第二种情况是当服务本身遇到内部故障时。Akka 要求所有 Actor 都被组织成一个树形的结构,即一个创造另一个 Actor 的 Actor 成为新 Actor 的父节点。这与操作系统将流程组织到树中的方式非常相似。就像处理过程一样,当一个 Actor 失败时,它的父 Actor 会得到通知,并且它可以对失败做出反应。另外,如果父 Actor 被停止,那么它的所有子 Actor 也将被递归地停止。这项服务称为监督,是 Akka 的核心概念。

一个监督者(父级节点)可以决定在某些类型的失败时重新启动其子 Actor,或者在其他失败时完全停止它们。子 Actor 永远不会默不作声地死去(除了进入一个无限循环之外),相反,他们要么失败,他们的父级可以对错误作出反应,要么他们被停止(在这种情况下,相关方会被自动通知)。总是有一个负责管理 Actor 的实体:它的父节点。从外部看不到重新启动:协作 Actor 可以在目标 Actor 重新启动时继续发送消息。