0%

Actor模型

面向对象语言中,对象之间通信,依靠的是对象方法。
Actor模型中,Actor之间通信,依靠的是消息。

Hello Actor模型

Actor模型本质上是一种计算模型,基本的计算单元称为Actor。在Actor模型中,所有的计算都是在Actor中执行的。在面向对象编程里面,一切都是对象;在Actor模型里,一切都是Actor,并且Actor之间是完全隔离的,不会共享任何变量。

并发问题的根源就在于共享变量,而Actor模型中Actor之间不共享变量,那用Actor模型解决并发问题,相当顺手。Actor模型是一种并发计算模型。

Java语言本身并不支持Actor模型,需要借助第三方类库,如Akka。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//该Actor收到消息message后,会打印Hello message
static class HelloActor extends UntypedActor {
@Override
public void onReceive(Object message) {
System.out.println("Hello" + message);
}
}

public static void main(String[] args) {
//创建Actor系统
ActorSystem system = ActorSystem.create("HelloSystem");
//床架HelloActor
ActorRef helloActor = system.actorOf(Props.create(HelloActor.class));
//发送消息给HelloActor
helloActor.tell("Actor", ActorRef.onSender());
}

先创建一个ActorSystem(Actor不能脱离ActorSystem存在);之后创建一个HelloActor,Akka中创建Actor并不是new一个对象出来,而是通过调用system.actorOf()方法创建的,该方法返回的是ActorRef,而不是HelloActor;最后通过调用ActorRef的tell()方法给HelloActor发送了一条消息“Actor”。

Actor模型和面向对象编程契合度非常高,完全可以用Actor类比面向对象编程里面的对象,而且Actor之间的通信方式完美的遵守了消息机制,而不是通过对象方法来实现对象之间的通信。

消息和对象方法的区别

Actor中的消息机制,可以类比现实世界里的写信。Actor内部有一个邮箱(Mailbox),接收到的消息都是先放到邮箱里,如果邮箱里有积压的消息,那么新收到的消息就不会马上得到处理。也因为Actor使用单线程处理消息,所以不会出现并发问题。可以把Actor内部的工作模式想象成只有一个消费者线程的生产者-消费者模式。

在Actor模型里,发送消息仅仅是把消息发出去而已,接收消息的Actor在接收到消息后,也不一定会立即处理。Actor中的消息机制完全是异步的;而调用对象方法,实际上是同步的,对象方法return之前,调用方会一直等待。

调用对象方法,需要持有对象的引用,所有的对象必须在同一个进程中。而在Actor中发送消息,类似于显示中的写信,只需要知道对方的地址就可以,发送消息和接收消息的Actor可以不在一个进程中,也可以不在同一台机器上。因此Actor模型不但适用于并发计算,还适用于分布式计算。

Actor的规范化定义

Actor是一种基础的计算单元,包括三部分能力:

  1. 处理能力,处理接收到的消息。
  2. 存储能力,Actor可以存储自己的内部状态,并且内部状态在不同Actor之间是绝对隔离的。
  3. 通信能力,Actor可以和其他Actor之间通信。

当一个Actor接收到一条消息后,可以做三件事:

  1. 创建更多的Actor;
  2. 发消息给其它Actor;
  3. 确定如何处理下一条消息。

    Actor具备存储灵力,它有自己的内部状态,所以可以把Actor看作一个状态机,把Actor处理消息看作是触发状态机的状态变化;而状态机的变化往往要基于上一个状态,触发状态机发生变化的时刻,上一个状态必须是确定的,所以确定如何处理下一条消息,本质上是改变内部状态。

在多线程中,由于可能存在竞态条件,所以根据当前状态确定如何处理下一条消息还是有难度的,需要使用各种同步工具,但在Actor模型里,由于是单线程处理,所以就不存在竞态条件问题。

用ACtor实现累加器

支持并发的累加器是最简单并且有代表性的并发问题,可以基于互斥锁方案实现,可以基于原子类实现,也可以用Actor来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//累加器
static class CounterActor extends UntypedActor {
private int counter = 0;
@Override
public void onReceive(Object message) {
//如果接收到的消息时数字类型,执行累加操作,否则打印counter的值
if(message instanceof Number) {
counter += ((Number) message).intValue();
} else {
System.out.println(counter);
}
}
}
public static void main(String[] args) throws InterruptedException {
//创建Actor系统
ActorSystem system = ActorSysyem.create("HelloSystem");
//4个线程生产消息
ExecutorService es = Executors.newFixedThreadPool(4);
//创建CounterActor
ActorRef counterActor = system.actorOf(Props.create(CounterActor.class));
//生产4*100000个消息
for(int i=0; i<4; i++) {
es.execute(()->{
for(int j=0; j<100000; j++) {
counterActor.tell(1, ActorRef.noSender());
}
})
}
//关闭线程池
es.shutdown();
//等待CounterActor处理完所有消息
Thread.sleep(1000);
//打印结果
counterActor.tell("", ActorRef.noSender());
//关闭Actor系统
system.shutdown();
}

CounterActor内部持有累计值counter,当CounterActor接收到一个数值型的消息message时,就将累加值counter += message;如果是其它类型的消息,则打印当前累计值counter。在main()方法中,启动了4个线程来执行累加操作。整个程序没有锁,也没有CAS,但是程序是线程安全的。

总结

Actor模型是一种简单的计算模型,Actor是最基本的计算单元,Actor之间是通过消息进行通信。Actor与面向对象编程(Object Oriented Programming,OOP)中的对象匹配度高。在面向对象编程里,系统由类似于生物细胞那样的对象构成,对象之间也是通过消息进行通信,所以在面向对象语言里使用Actor模型理所当然。

在Java领域,可以使用Akka来支持Actor模型,也可以使用Vert.x,相对于Akka的显示实现,Vert.x是Actor模型的隐式实现。

Actor可以创建新的Actor,这些Actor最终会呈现出一个树状结构,类似现实世界里的组织结构。Actor模型和现实世界一样都是异步模型,理论上不保证消息百分百送达,也不保证消息送达的顺序和发送的顺序一致,更无法保证消息会被百分百处理。这些都是使用Actor模型的成本。