跳转到主要内容
Chinese, Simplified

并发编程一直很棘手。从多个线程访问共享状态是一个中心问题,很容易出现难以捕获的错误。Java拥有安全和完美并发的所有工具,但是编译器会故意让开发人员编写危险的代码。现在迫切需要更高层次的框架来确保安全的并发编程。

Actor模型

actor模型是实现安全并发的方法之一。actors是可能具有可变状态并通常遵循标准规则的对象(Java意义上的类实例):

  • 一切都是actor。actors之间通过发送异步消息进行独占通信。没有共享状态、公共静态变量等。只有actors可以更改其状态。
  • 发送给actors的消息是按顺序处理的:尽管消息处理程序可能在不同的线程中被调用,但框架保证actors的状态更改是安全的,并且对所有后续消息处理程序都可见。

现有的actor框架有什么问题

然而,当我研究流行的Java actor框架时,我有一种奇怪的感觉。我试过akka,actor4j,kilim,quasar,reactors.io,orbit,offbynull/actors,edescortis/actor,vlingo actors,pcd actors,所有这些在我看来都是一样的……错了。

让我们来看看akka的一个简单的actor示例,akka是JVM最流行的actor框架之一:

public class MyActor extends AbstractActor {
     public Receive createReceive() {
        return receiveBuilder().match(SomeMessage.class, msg -> {
            doSomeCommandProcessing(msg);
            ... 
        }).match(AnotherMessage.class, anotherMessage -> {
            doAnotherMessageProcessing(anotherMessage);
            ...
        }).build();
    }
}

2019年,在这个Hello World的例子中,什么让我感到奇怪?

此API不是类型安全的。actor实现中的处理程序代码检查传入的消息类型(其中有一种instanceof)并正确地分派消息。但是我可以向任何actors发送任何消息,并且没有编译时检查来防止这种情况。

您需要从抽象类扩展。我讨厌从框架类扩展应用程序类。它产生了业务逻辑和库的丑陋组合,使我回到了使用ejbbean的旧时代。而且,如果我想的话,这不允许我从基类扩展。

必须为每种类型的消息创建一个类。在大多数情况下,这将是一个经典的不可变DTO,只有字段+构造函数+getter,所有这些烦人的样板行。不过没什么大不了的,JDK14记录类应该会更好,但正如我稍后将展示的那样,这是不必要的。

有趣的是,对于我所研究的大多数actors框架,API都有相同的问题(但是请参见下面的一些例外)。

更好的消息调用

A message is a Java object”仍然是传统actor框架的基石。让我们拒绝这个概念。Java有更好的消息发送方式:调用类方法。(注意,在Smalltalk中,向对象发送消息并调用对象的方法是等价的概念)。明显的好处是:

  • 该协议是静态定义的:方法签名定义actor类支持哪些方法(消息)以及接受哪些类型的消息(参数);
  • 不需要为消息类型创建DTO类,多个消息参数自然由多个方法参数实现。

使用这种方法,我们可能会针对问题1和3-太好了!

我可以使用这种方法找到2个Java actor框架:

Akka类型的actors使用JDK代理包装对象并返回一个安全接口,该接口将普通Java方法调用转换为异步调用。有了接口平方器及其实现SquarerImpl,就可以得到一个“安全”句柄:

Squarer safeSquarer = TypedActor.get(system)
    .typedActorOf(new TypedProps<SquarerImpl>(Squarer.class, SquarerImpl.class));

现在,对safesquare上void返回方法的调用将导致tell async actor调用,对值返回方法的调用将阻塞,直到该方法在其线程中返回为止(同步调用)。当一个方法返回一个未来时,真正的异步ask方法调用是可能的:

interface Squarer {
    void setParam(int param);            // async tell
    double square(double val);           // sync ask (blocking call)
    Future<Double> squareF(double val);  // async ask
}

Jumi Actors也有类似的方法,但它只支持tell消息:

ActorRef<Squarer> safeSquarer = actorThread.bindActor(Squarer.class,
    SquarerImpl.class);
safeSquarer.tell().setParam(param);

但是,这两个lib都不适合异步ask。打字actor对回归未来的要求并不优雅,Jumi根本不支持提问。

构建一个更好的actors框架

通过将方法调用包装在高阶函数中而不是代理接口,我们可以非常优雅地实现tell和ask:

IActorRef<Squarer> safeSquarer = system.actorOf(Squarer.class);
safeSquarer.tell(squarer -> squarer.setParam(param)); //async tell
safeSquarer.ask(squarer -> squarer.square(5),
    result -> System.out.println(result));            //async ask v1
CompletableFuture<Double> safeSquarer.ask(squarer ->
    squarer.square(5));                               //async ask v2

此API的优点:

  • async-tell和ask的优雅而安全的实现。上述三个问题都解决了。
  • 不需要有单独的接口和actor的实现。上例中的平方可以是接口或具体类。
  • 现在对actor类完全没有限制。无需扩展框架抽象类,无需返回特殊类型。任何类现在都可以是actor类,甚至是第三方类(将非线程安全库包装成一个单线程actor并从多个线程中使用它现在非常容易!)

我在一个名为actr的轻量级库中实现了这种方法:https://github.com/zakgof/actr

它没有按路径寻址actors或分布式actors支持等高级功能,但由于它是轻量级的,因此在一些简单的基准上与akka相比,它表现出了良好的性能:https://github.com/zakgof/akka-actr-benchmark

actr包含了一些简单的调度程序(以及在需要时提供自己的调度程序的能力)。它还支持JDK14 EA中的虚拟线程-欢迎您尝试。

原文:https://medium.com/@zakgof/type-safe-actor-model-for-java-7133857a9f72

本文:http://jiagoushi.pro/node/1021

讨论:请加入知识星球或者微信圈子【首席架构师圈】

Tags
 
Article
知识星球
 
微信公众号
 
视频号