akka更轻量级的异步框架_与Akka异步操作

系列前面的文章通过以下两种方法来实现并发:

  • 对多组数据并行执行相同的操作(与Java 8流一样)
  • 明确构造计算以异步执行某些操作,然后组合结果(与期货一样)

两者都是获得并发的极好方法,但是您必须在应用程序中明确设计它们。

现在,在接下来的几期中,我将重点介绍一种不同的并发方法,一种基于特定程序结构而不是显式编码的方法。 该程序结构是参与者模型 。 您将特别了解如何使用actor模型的Akka实现。 (阿卡是构建并行和分布式JVM的应用程序的工具包和运行时。)请参阅相关信息的链接,完整的示例代码的这篇文章。

演员模型基础

用于并发计算的参与者模型基于称为参与者的原语构建系统。 参与者对称为消息的输入采取行动。 动作可以包括更改参与者自己的内部状态,以及发送其他消息甚至创建其他参与者。 所有消息都是异步传递的,从而使消息发送者与接收者脱钩。 由于这种解耦,参与者系统在本质上是并发的:具有输入消息可用的任何参与者都可以不受限制地并行执行。

用Akka的术语来说,演员是通过消息互动的某种神经质的行为。 就像现实世界中的演员一样,Akka演员也希望获得一定程度的隐私。 您不能直接向Akka演员发送消息。 而是将消息发送到与邮局信箱等效的actor 参考 。 传入的消息通过引用路由到参与者的邮箱中,然后将消息传递给参与者。 Akka参与者甚至要求所有传入消息都是不育的(或用JVM术语表示,是不可变的 ),以避免受到其他参与者的污染。

与某些现实世界参与者的要求不同,Akka中这些看似过分的限制存在是有原因的。 对参与者使用引用可以防止消息交换之外的任何交互,这可能会破坏参与者模型核心的去耦。 Actor在执行中是单线程的(执行一个特定Actor实例的线程不超过一个),因此邮箱充当缓冲区,保留消息,直到可以对其进行处理为止。 消息的不可改变性(由于JVM的限制,Akka目前未强制执行,但已明确规定)意味着您无需担心会影响参与者之间共享数据的同步问题。 如果唯一的共享数据是不可变的,则不需要同步。

很高兴见到你

现在,您已经对角色模型和Akka细节进行了概述,现在该看一些代码了。 用问候打个招呼是个老生常谈,但是它确实给出了一种语言或系统的快速且易于理解的快照。 清单1显示了Scala中的Akka版本。

清单1.简单的Scala问候
import akka.actor._
import akka.util._

/** Simple hello from an actor in Scala. */
object Hello1 extends App {
  
  val system = ActorSystem("actor-demo-scala")
  val hello = system.actorOf(Props[Hello])
  hello ! "Bob"
  Thread sleep 1000
  system shutdown
  
  class Hello extends Actor {
    def receive = {
      case name: String => println(s"Hello $name")
    }
  }
}

清单1的代码分为两个单独的块,所有块都包含在Hello1应用程序对象中。 第一部分代码是Akka应用程序基础结构,它:

  1. 创建一个actor系统( ActorSystem(...)行)。
  2. 在系统内创建一个actor( system.actorOf(...)行,它返回所创建actor的actor引用)。
  3. 使用actor引用向actor发送消息( hello ! "Bob"行)。
  4. 等待一秒钟,然后关闭actor系统( system shutdown线)。

建议使用system.actorOf(Props[Hello])调用,使用专用于Hello actor类型的配置属性来创建actor实例。 对于这个简单的actor(扮演角色,只用一行对话),没有配置信息,因此Props对象没有参数。 如果要在角色上设置配置,则可以专门为该角色定义Props类,其中包括所有必要的信息。 (后面的示例显示了如何执行此操作。)

hello ! "Bob" hello ! "Bob"语句向创建的演员发送一条消息(在这种情况下,只是字符串Bob )。 ! 运算符是一种便捷的方式,可以通过即发即弃模式来向Akka中的演员发送消息。 如果您不喜欢专用的运算符样式,则可以使用tell()方法执行相同的操作。

第二段代码是Hello actor定义,从class Hello extends Actor 。 这个特定的参与者定义尽可能地简单。 它定义了必需的(对于所有参与者而言)部分功能receive ,该功能实现了对传入消息的处理。 ( receive是一个部分函数,因为它仅为某些输入定义-在这种情况下,仅是String消息输入。)为此参与者执行的处理方法是,每当接收到String消息时,使用消息值打印问候语。

你好用Java

清单2显示了普通Java中的清单1 Akka Hello。

清单2. Java中的Hello
import akka.actor.*;

public class Hello1 {

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("actor-demo-java");
        ActorRef hello = system.actorOf(Props.create(Hello.class));
        hello.tell("Bob", ActorRef.noSender());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) { /* ignore */ }
        system.shutdown();
    }

    private static class Hello extends UntypedActor {
        
        public void onReceive(Object message) throws Exception {
            if (message instanceof String) {
                System.out.println("Hello " + message);
            }
        }
    }
}

清单3显示了带有lambda的Java 8版本中的actor定义,以及支持lambda的ReceiveBuilder类所需的导入。 清单3可能更紧凑一些,但与清单2几乎相同。

清单3. Akka Hello的Java 8版本
import akka.japi.pf.ReceiveBuilder;
...
    private static class Hello extends AbstractActor {
        
        public Hello() {
            receive(ReceiveBuilder.
                match(String.class, s -> { System.out.println("Hello " + s); }).
                build());
        }
    }

相比清单2中,清单3的Java 8代码使用不同的基类- AbstractActor代替UntypedActor -并且还使用定义消息处理的替换方式的不同的方式。 ReceiveBuilder类使您可以使用lambda表达式来定义消息的处理,并具有类似于Scala的匹配语法。 如果您主要使用Scala进行开发,那么此技术可能会帮助您使Java Akka代码看起来更简洁一些,但是使用Java 8特定版本的好处似乎很少。

干嘛要等?

主应用程序代码在向系统发送消息之后,在关闭系统之前,包括Thread sleep 1000形式的等待。 您可能想知道为什么这是必要的。 毕竟,消息是微不足道的。 它不会立即传递给演员并在hello ! "Bob" hello ! "Bob"语句完成了吗?

这个问题的简短答案是“否”。 Akka actor异步运行,因此,即使目标actor与发送方位于同一JVM中,也不会立即执行目标actor。 而是,执行消息发送的线程将消息添加到目标参与者的邮箱。 依次将消息添加到邮箱将触发线程执行,以将消息从邮箱中取出并通过调用actor的receive方法对其进行处理。 但是,将邮件带出邮箱的线程通常与将邮件添加到邮箱的线程不同。

邮件传递时间和保证

除了简短的回答“为什么要等待?” 问题是更深层的原则。 Akka通过位置透明性支持actor远程处理,这意味着您的代码没有任何直接的方式来了解特定的actor是位于同一JVM内还是在云中某个地方的系统中运行。 但是,这两种情况在实际操作中显然会有很大不同。

Akka不保证会发送邮件。 这种非交付保证的哲学依据是Akka的核心原则之一。

区别之一是消息丢失。 Akka不保证将传递消息,这对于习惯于用于连接应用程序的消息传递系统的开发人员可能会感到惊讶。 这种无法交付的基本哲学依据是Akka的核心原则之一:为失败而设计。 作为一种过分的简化,请考虑传递保证会大大增加消息传输系统的复杂性,但是尽管如此,这些更为复杂的系统有时仍无法按预期工作,并且应用程序代码必须包含在恢复中。 这样,应用程序代码始终可以处理传递失败的情况,这使消息传递系统保持简单是很有意义的。

Akka 确实保证消息最多可以传递一次,并且从一个actor实例发送到另一个actor实例的消息永远不会被乱序接收。 保证的第二部分仅适用于特定的演员对,并且不具有关联性。 如果参与者A向参与者B发送消息,则这些消息将永远不会乱序。 如果演员A向演员C发送消息,情况也是如此。但是,如果演员B也向演员C发送消息(例如,通过将消息从A转发到C),则B的消息可能相对于消息混乱来自A。

清单1的代码中,消息丢失的可能性非常小,因为该代码在单个JVM中运行并且不会产生繁重的消息负载。 (沉重的消息负载可能导致消息丢失。例如,如果Akka的空间不足以存储消息,则别无选择,只能丢弃它们。)但是清单1的代码仍然没有对消息做任何假设,交付时间,并允许参与者系统进行异步操作。

演员和国家

Akka的演员模型非常灵活,可以适用于所有类型的演员。 您可以具有没有状态信息的参与者(如Hello1示例中所示),但是这些参与者往往等同于方法调用。 添加状态信息可以实现更加灵活的参与者功能。

清单1给出了一个参与者系统的完整(虽然很琐碎)示例,但是参与者被限制为总是一遍又一遍地做同样的事情。 甚至演员都只是在重复同一行而感到无聊,因此清单4通过向演员添加一些状态信息使事情变得更加有趣。

清单4. Polyglot Scala你好
object Hello2 extends App {
  
  case class Greeting(greet: String)
  case class Greet(name: String)
  
  val system = ActorSystem("actor-demo-scala")
  val hello = system.actorOf(Props[Hello], "hello")
  hello ! Greeting("Hello")
  hello ! Greet("Bob")
  hello ! Greet("Alice")
  hello ! Greeting("Hola")
  hello ! Greet("Alice")
  hello ! Greet("Bob")
  Thread sleep 1000
  system shutdown
  
  class Hello extends Actor {
    var greeting = ""
    def receive = {
      case Greeting(greet) => greeting = greet
      case Greet(name) => println(s"$greeting $name")
    }
  }
}

清单4的参与者知道如何处理在清单开头附近定义的两种不同类型的消息: Greeting消息和Greet消息,每种消息都包装一个字符串值。 修改后的Hello演员收到Greeting消息时,它将包装的字符串保存为greeting值。 当它收到Greet消息时,它将保存的问候语与Greet字符串结合起来以形成完整的消息。 这是您在运行此应用程序时看到的信息(尽管不一定按此顺序,因为actor的执行顺序不确定):

Hello Bob
Hello Alice
Hola Alice
Hola Bob

清单4代码中没有多少新内容,因此这里不包括Java版本。 你会在代码下载(见他们找到相关主题 )为com.sosnoski.concur.article5java.Hello2com.sosnoski.concur.article5java8.Hello2

属性和相互作用

真正的actor系统通过使用多个actor来完成工作,这些actor通过相互发送消息进行交互。 通常还需要向这些参与者提供配置信息,以准备其特定角色。 清单5建立在Hello示例中使用的技术的基础上,以显示actor配置和交互的简单版本。

清单5. Actor属性和交互
object Hello3 extends App {

  import Greeter._
  val system = ActorSystem("actor-demo-scala")
  val bob = system.actorOf(props("Bob", "Howya doing"))
  val alice = system.actorOf(props("Alice", "Happy to meet you"))
  bob ! Greet(alice)
  alice ! Greet(bob)
  Thread sleep 1000
  system shutdown

  object Greeter {
    case class Greet(peer: ActorRef)
    case object AskName
    case class TellName(name: String)
    def props(name: String, greeting: String) = Props(new Greeter(name, greeting))
  }

  class Greeter(myName: String, greeting: String) extends Actor {
    import Greeter._
    def receive = {
      case Greet(peer) => peer ! AskName
      case AskName => sender ! TellName(myName)
      case TellName(name) => println(s"$greeting, $name")
    }
  }
}

清单5展示了一个新的主演演员Greeter演员。 Greeter超越了Hello2示例,提供了以下步骤:

  • 传递的用于配置Greeter实例的属性
  • 一个Scala伴侣对象,用于定义配置属性和消息(如果您来自Java背景,请将伴侣对象视为与actor类同名的静态帮助器类)
  • Greeter actor实例之间发送的消息

此代码产生简单的输出:

Howya doing, Alice
Happy to meet you, Bob

如果尝试几次运行代码,则可能会看到相反的行。 此排序是Akka actor系统动态性质的另一个示例,其中消息的处理顺序是不确定的(除了我在“ 消息传递时间和保证 ”中讨论的几个重要例外)。

Java的Greeter

清单6显示了普通Java中的清单5 Akka Greeter代码。

清单6. Java中的Greeter
public class Hello3 {

    public static void main(String[] args) {
        ActorSystem system = ActorSystem.create("actor-demo-java");
        ActorRef bob = system.actorOf(Greeter.props("Bob", "Howya doing"));
        ActorRef alice = system.actorOf(Greeter.props("Alice", "Happy to meet you"));
        bob.tell(new Greet(alice), ActorRef.noSender());
        alice.tell(new Greet(bob), ActorRef.noSender());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) { /* ignore */ }
        system.shutdown();
    }
    
    // messages
    private static class Greet {
        public final ActorRef target;
        
        public Greet(ActorRef actor) {
            target = actor;
        }
    }
    
    private static Object AskName = new Object();
    
    private static class TellName {
        public final String name;
        
        public TellName(String name) {
            this.name = name;
        }
    }

    // actor implementation
    private static class Greeter extends UntypedActor {
        private final String myName;
        private final String greeting;
        
        Greeter(String name, String greeting) {
            myName = name;
            this.greeting = greeting;
        }
        
        public static Props props(String name, String greeting) {
            return Props.create(Greeter.class, name, greeting);
          }
        
        public void onReceive(Object message) throws Exception {
            if (message instanceof Greet) {
                ((Greet)message).target.tell(AskName, self());
            } else if (message == AskName) {
                sender().tell(new TellName(myName), self());
            } else if (message instanceof TellName) {
                System.out.println(greeting + ", " + ((TellName)message).name);
            }
        }
    }
}

清单7显示了带有lambda的Java 8版本。 同样,此版本在消息处理实现中更为紧凑,但在其他方面相同。

清单7. Java 8版本
import akka.japi.pf.ReceiveBuilder;
...
    private static class Greeter extends AbstractActor {
        private final String myName;
        private final String greeting;
        
        Greeter(String name, String greeting) {
            myName = name;
            this.greeting = greeting;
            receive(ReceiveBuilder.
                match(Greet.class, g -> { g.target.tell(AskName, self()); }).
                matchEquals(AskName, a -> { sender().tell(new TellName(myName), self()); }).
                match(TellName.class, t -> { System.out.println(greeting + ", " + t.name); }).
                build());
        }
        
        public static Props props(String name, String greeting) {
            return Props.create(Greeter.class, name, greeting);
          }
    }

传递属性

Akka使用Props对象将配置属性传递给角色。 每个Props实例都包装actor类所需的构造函数参数的副本以及对该类的引用。 该信息可以通过两种方式传递给Props构造函数。 清单5的示例将actor的构造函数作为传递参数传递给Props构造函数。 请注意,这种方式不会立即调用构造函数并传递结果。 它通过构造函数调用(如果您来自Java背景,这可能看起来很奇怪)。

将actor配置传递给Props构造函数的另一种方法是,将actor的类作为第一个参数,而将actor的构造函数参数作为其余参数。 对于清单5的示例,这种调用形式是Props(classOf[Greeter], name, greeting)

无论使用哪种形式的Props构造函数,传递给新生Actor的值都必须可序列化,以便可以在必要时通过网络将Props发送到actor实例将运行的任何位置。 对于清单5中使用的“按名称传递”构造函数调用,当需要将其发送到JVM之外时,将对调用的关闭进行序列化。

Akka建议在Scala代码中创建Props对象的做法是在伴随对象中定义一个工厂方法,如清单5所示 。 当您对Props使用按名称传递构造函数调用方法时,此技术可以防止意外关闭this对Actor对象的引用所引起的任何可能的问题。 伴随对象也是定义参与者将收到的消息的好地方,因此所有关联的信息都放在一个地方。 对于Java actor,actor类内部的静态构造方法很好用,如清单6所示

演员发送消息

清单5的 Greeter演员中的每个演员都配置了一个名字和问候,但是当被告知与另一个演员打招呼时,首先需要找出另一个演员的名字。 Greeter演员通过向另一位演员发送单独的消息: AskName消息来完成此任务。 AskName消息本身不包含任何信息,但是接收到它的Greeter实例知道将使用带有TellName发送者名称的TellName消息进行响应。 当第一个Greeter收到一条TellName消息时,它会打印出问候语。

发送给actor的每条消息都带有Akka提供的一些附加信息,最著名的是消息发件人的ActorRef 。 您可以在消息处理期间的任何时候通过调用参与者的基类上定义的sender()方法来访问此发送者信息。 Greeter者参与者在处理AskName消息时使用发件人引用,以便将TellName响应发送给正确的TellName

Akka允许您代表另一个演员发送消息(身份盗用的一种良性形式),以便接收消息的演员将另一个演员视为发送者。 这通常是在actor系统中使用的有用功能,特别是对于请求-响应类型的消息交换,在该消息交换中,您希望响应除了发送给actor的actor外,还可以传递到其他地方。 默认情况下,由应用程序代码在actor外部发送的消息将使用特殊的Akka actor(称为deadletter actor)作为发送者。 死信参与者也可以在无法将消息传递给参与者的任何时候使用,通过打开适当的日志记录(在下一部分中将介绍),提供了一种便捷的方法来跟踪参与者系统中无法传递的消息。

打字演员

您可能会注意到,示例消息序列中的任何地方都没有任何类型信息明确表明消息的​​目标是Greeter实例。 Akka演员及其交流的消息通常就是这种情况。 甚至用于标识消息的目标actor的ActorRef也没有ActorRef

对无类型的actor系统进行编程具有实际优势。 您可以定义参与者类型(例如,根据参与者可以处理的消息集),但是这样做会产生误导。 在Akka中,参与者可以更改其行为(在下一部分中将对此进行详细说明),因此不同的消息集可能适用于不同的参与者状态。 类型也倾向于妨碍参与者模型的优雅简洁性,该模型将所有参与者视为至少具有处理任何消息的潜力。

尽管如此,当您真正想要使用Akka时,Akka仍然提供类型化的actor支持。 当您在参与者和非参与者代码之间建立接口时,此支持最有用。 您可以定义一个接口,非角色代码可以使用该接口来与角色一起使用,使角色看起来更像是普通的程序组件。 考虑到即使是从actor系统外部直接将消息直接发送到actor的过程也很容易,对于大多数用途而言,这可能比它值得的麻烦更多(到目前为止,您可以从任何示例应用程序中看到,其中非actor代码都可以看到)一直在发送消息),但是可以使用该选项真是太好了。

消息和可变性

Akka希望您确定自己不会在参与者之间意外共享可变数据。 如果这样做的话,结果可能会很糟糕-不及与鬼打交道时越过质子束流的情况( Ghostbusters参考,如果您是未参与其中的人)那样糟糕,但仍然非常糟糕。 共享可变数据的问题是参与者在单独的线程中运行。 如果您在角色之间共享可变数据,则运行角色的线程之间将没有协调,因此他们将看不到其他线程在做什么,并且可能以许多不同的方式相互竞争。 如果您运行的是分布式系统,那么每个参与者将拥有自己的可变数据副本,问题将变得更加严重。

因此,消息必须是不可变的,而不仅仅是在表面上。 如果有任何对象是消息数据的一部分,则这些对象也必须是不可变的,以此类推,直到消息引用的所有内容都关闭为止。 Akka目前无法执行此要求,但是Akka开发人员希望在将来的某个时候施加限制。 如果您希望代码在Akka的将来版本中保持可用,则必须立即注意此要求。

问与说

清单5代码使用标准的tell操作发送消息。 使用Akka,您还可以使用ask消息模式作为辅助操作。 ask操作(由?运算符显示,或通过使用ask函数显示)发送带有Future的消息作为响应。 清单8显示了清单5的代码经过重组,以使用ask而不是tell

清单8.使用ask
import scala.concurrent.duration._
import akka.actor._
import akka.util._
import akka.pattern.ask

object Hello4 extends App {

  import Greeter._
  val system = ActorSystem("actor-demo-scala")
  val bob = system.actorOf(props("Bob", "Howya doing"))
  val alice = system.actorOf(props("Alice", "Happy to meet you"))
  bob ! Greet(alice)
  alice ! Greet(bob)
  Thread sleep 1000
  system shutdown

  object Greeter {
    case class Greet(peer: ActorRef)
    case object AskName
    def props(name: String, greeting: String) = Props(new Greeter(name, greeting))
  }

  class Greeter(myName: String, greeting: String) extends Actor {
    import Greeter._
    import system.dispatcher
    implicit val timeout = Timeout(5 seconds)
    def receive = {
      case Greet(peer) => {
        val futureName = peer ? AskName
        futureName.foreach { name => println(s"$greeting, $name") }
      }
      case AskName => sender ! myName
    }
  }
}

在清单8的代码中, TellName消息已替换为askask操作返回的Future[Any]的类型为Future[Any] ,因为编译器对返回的结果一无所知。 将来完成时, foreach使用import system.dispatcher语句定义的隐式调度程序执行println 。 如果将来未在允许的超时内完成响应消息(另一个隐式值,在这种情况下定义为五秒),它将以超时异常完成。

在幕后, ask模式创建了一个专门的单发演员,充当消息交换中的中介。 中介会传递一个Promise和要发送的消息以及目标参与者引用。 它发送消息,然后等待预期的响应消息。 收到响应后,它就会兑现承诺并完成原始演员使用的未来。

使用ask方法有一些局限性。 特别是,为避免暴露角色状态(并可能导致线程问题),您必须确保在将来完成时所执行的代码中不要使用角色的任何可变状态。 实际上,在参与者之间发送的消息通常使用tell模式。 当您具有需要从参与者(无论是键入的还是未键入的)返回响应的应用程序代码(例如启动参与者系统并创建初始参与者的主程序)时,发生询问模式更为有用的一种情况。

位角色

只要能帮助您干净地处理异步操作,就不要在设计中引入新角色。

由问号模式创建的单发演员是使用Akka时要牢记的良好设计原则的示例。 通常需要构造您的actor系统,以便由专门为该特定目的而设计的特殊actor执行中间处理步骤。 一个常见的例子是在进入下一处理阶段之前,需要合并不同的异步结果。 如果将消息用于不同的结果,则可以让演员收集结果,直到一切准备就绪,然后再进行下一个阶段。 这基本上是对询问模式使用的单发演员的概括。

Akka actor是轻量级的(每个actor实例大约300到400字节,再加上actor类使用的任何存储空间),因此您可以安全地设计自己的设计,以在适当的时候使用许多actor。 使用专门的参与者可以使您的代码简单易懂,与编写顺序程序相比,这在编写并发程序时更具优势。 每当它有助于您干净地处理异步操作时,请立即在您的设计中引入一个新角色。

中场休息

Akka是一个功能强大的系统,但是Akka和actor模型通常需要与直接过程代码不同的编程样式。 使用过程代码,您将拥有一个程序结构,其中所有调用都是确定性的,并且可以查看程序的整个调用树。 在参与者模型中,乐观地发出了消息,但不能保证消息会一直被发送,而且事情发生的顺序通常很难确定。 actor模型的好处是一种结构化应用程序的简便方法,以实现高并发性和可伸缩性,这一点我将在以后的文章中再次讨论。

我希望本文能给您足够的Akka味道,以激发您的胃口。 下次,我将带您更深入地了解actor系统和actor交互,包括如何轻松跟踪系统中actor之间的交互。


翻译自: https://www.ibm.com/developerworks/java/library/j-jvmc5/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值