Actor模型:面向对象原生的并发模型

前言

感谢极客时间 王宝令老师的 并发系列课程

有门计算机专业课叫做面向对象编程, 按照面向对象编程理论,对象之间通信需要依靠消息,而实际上,像 C++、Java 这些面向对象的语言, 对象之间的通信,依靠的是对象方法。对象方法和过程语言里的函数本质上没有区别,有入参、出参,思维方式很相似,使用起来都很简单。那面向对象里的消息是否就等价于面向对象语言里的对象方法呢?很长一段时间里,我都以为对象方法是面向对象理论中消息的一种实现, 直到接触到 Actor 模型,才明白消息压根不是这个实现法。

Hello Actor 模型

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

当看到 “不共享任何变量”的时候, 你一定会眼前一亮,并发问题的根源就在于共享变量,而 Actor 模型中 Actor 之间不共享变量, 那用 Actor 模型解决并发问题, 一定是相当顺手,的却是这样,所以很多人把Actor 模型定义为一种并发计算模型。其实Actor 模型早在1973年就 被提出来了,只是知道最近几年才被关注,一个主要原因就在于他是解决并发问题的利器,而最近几年随着多核处理器的发展,并发问题被推到了风口浪尖。

但是 Java 语言本身并不支持 Actor 模型, 所以如果你想在 Java 语言里使用 Actor 模型, 就要借助第三方类库,目前完备支持Actor 模型而且比较成熟的类库就是Akka 了,在详细介绍 Actor 模型之前,我们就先基于 Akka 写一个 Hello World 程序,让你对 Actor 模型先有个感官的印象。

以Java项目为例,首先 Pom 中加入依赖:

        
            com.typesafe.akka
            akka-actor_2.13
            2.5.23
        

Maven 依赖版本查询


public class AkkaApplicationMain {

    public static void main(String[] args) {

        ActorSystem system = ActorSystem.create("HelloSystem");

        ActorRef helloActor = system.actorOf(Props.create(HelloActor.class));
        //发送消息给Actor
        helloActor.tell("Actor", ActorRef.noSender());

    }

    static class HelloActor extends UntypedAbstractActor {
        @Override
        public void onReceive(Object message) throws Throwable {
            System.out.println("Helllo : " + message);
        }
    }
}

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

通过这个例子,你会发现Actor 模型和面向对象编程契合度非常高,完全可以用Actor 类比面向对象编程里面的对象,而且Actor 之间的通信方式完美遵循了消息机制,而不是通过对象方法来实现对象之间的通信。那Actor 中的消息机制和面向对象语言里的对象方法有什么区别呢?

消息和对象方法的区别

在没有计算机的时代,异地的交流主要靠写信,但是信件发出去之后,也许会在邮寄过程中丢失了,也可能寄到后,对方一直没有时间写回信……总之任何结果都不可控了。

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

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

除此之外,调用对象方法还需要持有对象引用,所有的对象必须在同一个进程中,而在Actor 中发送消息,类似与写信,只要知道对方地址就好了,发送和接收消息的Actor 可以不在一个进程中,也可以不在同一台机器上。一位内Actor 不仅适用于并发计算,而且适用于分布式计算。

Actor 的规范化定义

通过上面介绍,你应该已经对Actor有感官印象了,下面我们来看看Actor 的规范胡定义;Actor 是一种基础的计算单元,包括三部分能力,分别是:

  1. 处理能力,处理接收到的信息
  2. 发送消息给其他的Actor
  3. 确定如何处理下一条消息

其中前两条还是很好理解的,最后一条该如何理解呢?就是我们前面说过的Actor 具备存储能力,它有自己的内部状态,所以你可以把Actor 看作一个状态机,把Actor 处理消息看作是触发状态机的状态变化,而状态的变化往往要基于上一个状态,触发状态机发生变化的时刻,上一个状态必须是明确的,所以确定如何处理下一条消息,本质上不过是改变内部状态。

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

用 Actor 实现累加器

支持并发的累加器可能是最具代表性的并发问题了,可以基于互斥锁实现。也可以基于原子类来实现,但今天我们要尝试用Actor 来实现。

public class CounterActorApplication {

    public static void main(String[] args) throws Exception {

        ActorSystem system = ActorSystem.create("CounterSystem");
        //4 个线程生产消息
        ExecutorService service = Executors.newFixedThreadPool(4);

        ActorRef counterActor = system.actorOf(Props.create(CounterActor.class));

        for (int i = 0; i < 4; i++) {
            service.execute(() -> {
                for (int j = 0; j < 10000; j++) {
                    counterActor.tell(1, ActorRef.noSender());
                }
            });
        }
        //关闭线程池
        service.shutdown();
        //等待执行结果
        Thread.sleep(10000);
        //打印结果
        counterActor.tell("", ActorRef.noSender());

    }

    static class CounterActor extends UntypedAbstractActor {

        private int counter = 0;

        //如果接收到的是数字型,则进行累加操作
        //否则直接打印
        @Override
        public void onReceive(Object message) throws Throwable {
            if (message instanceof Number) {
                counter += ((Number) message).intValue();
            } else {
                System.out.println(counter);
            }
        }
    }
}

输出结果:
40000

总结

Actor 模型是一种非常简单的计算模型,其中Actor 是最基本的计算单元,Actor 之间是通过消息进行通信,Actor 与面向对象编程中的对象匹配度非常高,在面向对象编程里面,系统由类似于生物细胞那样的构成,对象之间也是通过消息进行通信,所以在面向对象语言里使用Actor 模型基本上不会有违和感。

在Java 领域,除了可用Akka 来支持Actor 模型外,还可以使用Vert.x, 不过相对来说Vert.x, 更像是Actor 模型的隐式实现,对应关系不像Akka 那样明显,不过本质上也是一种Actor 模型。

Actor 可以创建新的Actor ,这些Actor 最终会呈现出一个树状结构,非常像现实世界里的组织结构,所以利用Actor 模型来对程序进行建模,和现实世界匹配度非常高,Actor 模型跟现实世界一样都是异步模型,理论上不保证消息百分百送达,也不保证消息的顺序和发送的顺序一致的。甚至不会保证消息百分百处理,虽然Actor 实现厂商都在努力解决这些问题。

你可能感兴趣的:(Actor模型:面向对象原生的并发模型)