blog/source/_posts/actor-pattern.md
2021-05-18 08:51:11 +08:00

82 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Actor模式
tags:
- 架构知识
- 软件设计理论
- Actor
- 高并发
- 共享内存
- 分布式
categories:
- - 架构知识
- 软件设计理论
keywords: 'Actor,高并发,分布式,共享,并发编程,模式'
date: 2021-05-18 08:50:40
---
Actor模式是于1973年提出的作为一个分布式并发编程模型在Erlang中得到了广泛的应用。在Actor模式中Actor是一个并行计算的模型是设计用于大量独立处理器联合进行并行计算的。要理解Actor模式借用面向对象的一句话就可以了万物皆Actor。<!-- more -->
## 原理
Actor模式将Actor作为通用原语Actor作为参与者需要对接收到的消息做出响应同时也可以向更多的参与者发出消息。Actor模式实际上是一个用于并行计算的概念模型它所定义的是系统的组件需要如何动作和如何交互以及它们做需要遵循的规则。
在Actor模式中每个Actor都是完全独立的就像是舞台上的一个个演员一样可以同时执行它们的操作。Actor之间通过消息来与外界通信这也就像是舞台上的演员通过语言相互交流一样。Actor之间的交互消息是异步的但Actor处理消息的过程是同步的而且一次只能处理一条消息。基于Actor的这个特点每个Actor都会有自己的一个邮箱这个邮箱用于缓存其他Actor发来的消息这样可以使其他Actor发送过来的消息不至于丢失。
一个Actor在接收到消息以后可以执行的动作通常有以下三个
- 发送其他消息给其他的Actor不限于仅发送一条消息。
- 创建一系列新的Actor。
- 为下一个接收到的信息指定行为。
Actor在完成这三个动作时没有固定的顺序这也是并发编程的特点。Actor会根据接收到的消息做出不同的处理行为。
Actor模式的另一个优势是可以消除共享状态。传统多线程并发编程的数据共享是通过共享内存区域来实现的在访问共享内存区域的时候往往需要加锁来使线程获得共享内存区域的临时独占权。加锁的操作不仅需要消耗时间而且还会带来一些无法预料的问题例如死锁。而Actor模式则不同Actor每次只能处理一条消息所以在Actor内部可以安全的处理状态不必使用锁的机制。
一个Actor是由状态state、行为behavior和邮箱mailbox组成的Actor中的着三种元素都有以下特性
- 状态是Actor对象内部所存储的信息仅由Actor自身进行管理。
- 行为是Actor对象中的业务逻辑可以通过Actor接收到的消息改变Actor的状态。
- 邮箱是Actoe对象之间的通信桥梁在实现上邮箱实际上就是一个FIFO的消息队列。
通过Actor对象中这三种元素的特征可以看出Actor模式都使用哪些原则来解决并发编程中的痛点。
1. 所有的Actor对象都是本地的无法从外部访问。
1. 所有的Actor必须通过消息进行通信。
1. Actor的行为主要包括响应消息、退出Actor、改变Actor的内部状态、发送消息到一个或者多个Actor。
1. Actor可能会阻塞自己的运行但是不能阻塞所在的线程。
当所有的并发参与者都按照以上原则进行工作时,并发编程中的那些诸如共享内存访问、死锁等问题就都将得到解决。
## 调度流程
在使用Actor模式的系统中首先会存在一个未处理的任务集这个任务集中的每个任务都有至少以下三个属性标识
- 任务的标记`tag`用以将任务与其他的任务区别开。
- 任务要送达的目的地`target`,用于标识任务需要送达的邮箱。
- 任务的通讯信息`communication`用于向Actor提供处理任务所需要的信息也同时包含用来处理任务的Actor本身。
这三个属性组成了一个任务的基本元素并且可以在Actor之间传递所以一个任务也可以被看作是一条消息。对于一条消息的处理Actor通常都需要进行调度Actor模式通常会有两种调度模式一种基于线程一种基于事件。
基于线程的调度方式比较容易理解就是将每个Actor都放入一个线程里。如果这个Actor要处理的消息队列是空的那么这个Actor的线程将被阻塞在等待消息输入的位置上。但是在这种情况下一个系统中可以创建和控制的线程数量是有限的而且系统在不同线程之间进行切换也是有资源成本的所以大多数Actor模式都没有选择基于线程的调度方式。
而基于事件的调度方式则不同虽然每个Actor也是运行在一个独立的线程中但是只有在这个Actor接收到消息的时候系统才会为其分配线程。这样系统就可以使用比较少的线程资源创建和管理更多的Actor。所以对于一个Actor其在运行的时候会是以下这样一个状态循环。
```plantuml
@startuml
hide empty description
state "等待消息" as waiting
state "系统分配线程" as dispatch
state "处理消息" as processing
state "发送消息给其他Actor" as sending
state "释放占据线程" as dispose
waiting -> dispatch: 收到消息
dispatch -> processing
processing -> sending
sending -> dispose
dispose -> waiting
@enduml
```
于是根据前文的Slogan万物皆Actor可以把一个系统中的任务分解为一些更细力度的小任务每个任务交由一个Actor去处理这样在一些可以并行处理的场合Actor模式就会自然的形成并发减少任务的完成时间。而且因为Actor之间不需要访问共享内存区域所以Actor中的处理过程也无须考虑锁的机制。