Einführung in Akka Actors in Java

1. Einleitung

Akka ist eine Open-Source-Bibliothek, mit deren Hilfe Sie mithilfe von Actor Model problemlos gleichzeitige und verteilte Anwendungen mit Java oder Scala entwickeln können.

In diesem Tutorial werden die grundlegenden Funktionen wie das Definieren von Akteuren, wie sie kommunizieren und wie wir sie töten können vorgestellt . In den letzten Anmerkungen werden wir auch einige Best Practices für die Arbeit mit Akka erwähnen.

2. Das Schauspieler-Modell

Das Schauspieler-Modell ist für die Informatik-Community nicht neu. Es wurde erstmals 1973 von Carl Eddie Hewitt als theoretisches Modell für die gleichzeitige Berechnung eingeführt.

Es begann seine praktische Anwendbarkeit zu zeigen, als die Softwareindustrie begann, die Fallstricke der Implementierung gleichzeitiger und verteilter Anwendungen zu erkennen.

Ein Akteur repräsentiert eine unabhängige Recheneinheit. Einige wichtige Merkmale sind:

  • Ein Akteur kapselt seinen Status und einen Teil der Anwendungslogik
  • Akteure interagieren nur durch asynchrone Nachrichten und niemals durch direkte Methodenaufrufe
  • Jeder Akteur hat eine eindeutige Adresse und ein Postfach, in dem andere Akteure Nachrichten übermitteln können
  • Der Akteur verarbeitet alle Nachrichten in der Mailbox nacheinander (die Standardimplementierung der Mailbox ist eine FIFO-Warteschlange).
  • Das Akteursystem ist in einer baumartigen Hierarchie organisiert
  • Ein Schauspieler kann andere Schauspieler erstellen, Nachrichten an jeden anderen Schauspieler senden und sich selbst stoppen oder jeder Schauspieler wurde erstellt

2.1. Vorteile

Das Entwickeln einer gleichzeitigen Anwendung ist schwierig, da wir uns mit Synchronisation, Sperren und gemeinsam genutztem Speicher befassen müssen. Durch die Verwendung von Akka-Akteuren können wir problemlos asynchronen Code schreiben, ohne dass Sperren und Synchronisation erforderlich sind.

Einer der Vorteile der Verwendung von Nachrichten anstelle von Methodenaufrufen besteht darin, dass der Absender-Thread nicht blockiert, um auf einen Rückgabewert zu warten, wenn er eine Nachricht an einen anderen Akteur sendet . Der empfangende Akteur antwortet mit dem Ergebnis, indem er eine Antwortnachricht an den Absender sendet.

Ein weiterer großer Vorteil der Verwendung von Nachrichten besteht darin, dass wir uns nicht um die Synchronisierung in einer Multithread-Umgebung kümmern müssen. Dies liegt an der Tatsache, dass alle Nachrichten nacheinander verarbeitet werden .

Ein weiterer Vorteil des Akka-Akteurmodells ist die Fehlerbehandlung. Durch die Organisation der Akteure in einer Hierarchie kann jeder Akteur seinen Elternteil über den Fehler informieren und entsprechend handeln. Der übergeordnete Akteur kann entscheiden, die untergeordneten Akteure zu stoppen oder neu zu starten.

3. Setup

Um die Akka-Schauspieler zu nutzen, müssen wir die folgende Abhängigkeit von Maven Central hinzufügen:

 com.typesafe.akka akka-actor_2.12 2.5.11  

4. Erstellen eines Schauspielers

Wie bereits erwähnt, sind die Akteure in einem Hierarchiesystem definiert. Alle Akteure, die eine gemeinsame Konfiguration haben, werden von einem ActorSystem definiert .

Im Moment definieren wir einfach ein ActorSystem mit der Standardkonfiguration und einem benutzerdefinierten Namen:

ActorSystem system = ActorSystem.create("test-system"); 

Obwohl wir noch keine Akteure erstellt haben, enthält das System bereits drei Hauptakteure:

  • Der Root-Guardian-Akteur hat die Adresse „/“, die, wie der Name schon sagt, die Wurzel der Akteursystemhierarchie darstellt
  • der User Guardian Actor mit der Adresse "/ user". Dies ist das übergeordnete Element aller von uns definierten Akteure
  • der Systemwächter mit der Adresse „/ system“. Dies ist das übergeordnete Element für alle vom Akka-System intern definierten Akteure

Jeder Akka-Akteur erweitert die AbstractActor- Abstract-Klasse und implementiert die createReceive () -Methode für die Verarbeitung eingehender Nachrichten von anderen Akteuren:

public class MyActor extends AbstractActor { public Receive createReceive() { return receiveBuilder().build(); } }

Dies ist der grundlegendste Schauspieler, den wir erstellen können. Es kann Nachrichten von anderen Akteuren empfangen und verwirft diese, da im ReceiveBuilder keine übereinstimmenden Nachrichtenmuster definiert sind. Wir werden später in diesem Artikel über den Nachrichtenmusterabgleich sprechen.

Nachdem wir unseren ersten Schauspieler erstellt haben, sollten wir ihn in das ActorSystem aufnehmen :

ActorRef readingActorRef = system.actorOf(Props.create(MyActor.class), "my-actor");

4.1. Schauspieler-Konfiguration

Die Props- Klasse enthält die Schauspieler-Konfiguration. Wir können Dinge wie den Dispatcher, das Postfach oder die Bereitstellungskonfiguration konfigurieren. Diese Klasse ist unveränderlich und somit threadsicher, sodass sie beim Erstellen neuer Akteure gemeinsam genutzt werden kann.

Es wird dringend empfohlen und als bewährte Methode angesehen, die Factory-Methoden im Actor-Objekt zu definieren, mit denen das Props- Objekt erstellt wird.

Als Beispiel definieren wir einen Akteur, der eine Textverarbeitung durchführt. Der Akteur erhält ein String- Objekt, auf dem er die Verarbeitung durchführt:

public class ReadingActor extends AbstractActor { private String text; public static Props props(String text) { return Props.create(ReadingActor.class, text); } // ... }

Um eine Instanz dieses Aktortyps zu erstellen, verwenden wir einfach die Factory-Methode props () , um das String- Argument an den Konstruktor zu übergeben:

ActorRef readingActorRef = system.actorOf( ReadingActor.props(TEXT), "readingActor");

Nachdem wir nun wissen, wie ein Akteur definiert wird, wollen wir sehen, wie er innerhalb des Akteursystems kommuniziert.

5. Actor Messaging

To interact with each other, the actors can send and receive messages from any other actor in the system. These messages can be any type of object with the condition that it's immutable.

It's a best practice to define the messages inside the actor class. This helps to write code that is easy to understand and know what messages an actor can handle.

5.1. Sending Messages

Inside the Akka actor system messages are sent using methods:

  • tell()
  • ask()
  • forward()

When we want to send a message and don't expect a response, we can use the tell() method. This is the most efficient method from a performance perspective:

readingActorRef.tell(new ReadingActor.ReadLines(), ActorRef.noSender()); 

The first parameter represents the message we send to the actor address readingActorRef.

The second parameter specifies who the sender is. This is useful when the actor receiving the message needs to send a response to an actor other than the sender (for example the parent of the sending actor).

Usually, we can set the second parameter to null or ActorRef.noSender(), because we don't expect a reply. When we need a response back from an actor, we can use the ask() method:

CompletableFuture future = ask(wordCounterActorRef, new WordCounterActor.CountWords(line), 1000).toCompletableFuture();

When asking for a response from an actor a CompletionStage object is returned, so the processing remains non-blocking.

A very important fact that we must pay attention to is error handling insider the actor which will respond. To return a Future object that will contain the exception we must send a Status.Failure message to the sender actor.

This is not done automatically when an actor throws an exception while processing a message and the ask() call will timeout and no reference to the exception will be seen in the logs:

@Override public Receive createReceive() { return receiveBuilder() .match(CountWords.class, r -> { try { int numberOfWords = countWordsFromLine(r.line); getSender().tell(numberOfWords, getSelf()); } catch (Exception ex) { getSender().tell( new akka.actor.Status.Failure(ex), getSelf()); throw ex; } }).build(); }

We also have the forward() method which is similar to tell(). The difference is that the original sender of the message is kept when sending the message, so the actor forwarding the message only acts as an intermediary actor:

printerActorRef.forward( new PrinterActor.PrintFinalResult(totalNumberOfWords), getContext());

5.2. Receiving Messages

Each actor will implement the createReceive() method, which handles all incoming messages. The receiveBuilder() acts like a switch statement, trying to match the received message to the type of messages defined:

public Receive createReceive() { return receiveBuilder().matchEquals("printit", p -> { System.out.println("The address of this actor is: " + getSelf()); }).build(); }

When received, a message is put into a FIFO queue, so the messages are handled sequentially.

6. Killing an Actor

When we finished using an actor we can stop it by calling the stop() method from the ActorRefFactory interface:

system.stop(myActorRef);

We can use this method to terminate any child actor or the actor itself. It's important to note stopping is done asynchronously and that the current message processing will finish before the actor is terminated. No more incoming messages will be accepted in the actor mailbox.

By stopping a parent actor, we'll also send a kill signal to all of the child actors that were spawned by it.

When we don't need the actor system anymore, we can terminate it to free up all the resources and prevent any memory leaks:

Future terminateResponse = system.terminate();

This will stop the system guardian actors, hence all the actors defined in this Akka system.

We could also send a PoisonPill message to any actor that we want to kill:

myActorRef.tell(PoisonPill.getInstance(), ActorRef.noSender());

The PoisonPill message will be received by the actor like any other message and put into the queue. The actor will process all the messages until it gets to the PoisonPill one. Only then the actor will begin the termination process.

Another special message used for killing an actor is the Kill message. Unlike the PoisonPill, the actor will throw an ActorKilledException when processing this message:

myActorRef.tell(Kill.getInstance(), ActorRef.noSender());

7. Conclusion

In this article, we presented the basics of the Akka framework. We showed how to define actors, how they communicate with each other and how to terminate them.

We'll conclude with some best practices when working with Akka:

  • Verwenden Sie tell () anstelle von ask (), wenn die Leistung ein Problem darstellt
  • bei Verwendung fragen () sollten wir immer Ausnahmen behandeln , indem eine Sende Failure Meldung
  • Akteure sollten keinen veränderlichen Zustand teilen
  • Ein Schauspieler sollte nicht innerhalb eines anderen Schauspielers deklariert werden
  • Schauspieler werden nicht automatisch gestoppt, wenn sie nicht mehr referenziert werden. Wir müssen einen Akteur explizit zerstören, wenn wir ihn nicht mehr benötigen, um Speicherlecks zu verhindern
  • Nachrichten, die von Schauspielern verwendet werden, sollten immer unveränderlich sein

Wie immer ist der Quellcode für den Artikel auf GitHub verfügbar.