Aktor-Modell und das Akka-Framework

Parallele Programmierung (3IB)

Prof. Dr.-Ing. Sandro Leuchter
Hochschule Mannheim, Fakultät für Informatik
Wintersemester 2024/2025

 

Dieses Werk ist lizenziert unter einer Creative Commons „Namensnennung – Nicht-kommerziell – Weitergabe unter gleichen Bedingungen 4.0 International“ Lizenz.

Überblick

  • Aktor-Modell
    • Aktoren, Nachrichten, Nachrichtenverarbeitung
  • Aktoren überwachen Aktoren
    • “let it crash” statt Exception Handling, Supervisors, Error-Kernel Design Pattern
  • Akka als Umsetzung des Aktor-Modells
    • Aktor programmieren, der auf Nachrichten reagieren kann; Aktoren erzeugen
    • Hierarchie von Aktoren, Nachrichten versenden, Monitoring (Supervisor-Aktor)
  • Beispiel mit 3 Aktor-Typen und 4 unterschiedlichen Nachrichtenarten
  • Router in Akka
  • Beispiel von vorher mit Router

Aktor-Modell

Aktor-Modell

  • Idee aus den 70er Jahren (Hewitt, Bishop und Steiger (1973))
    • lange exotisch (u.a. Erlang)
    • hauptsächlich in Kommunikationssyst.: Telefonvermittlung, Whatsapp, RabbitMQ
    • inzwischen “Hot Topic” (u.a. Swift)
  • im Prinzip nebenläufige Objekte
  • eigener Zustand, eigenes Verhalten
  • Kommunikation mit anderen Aktoren durch Senden von Nachrichten (Methodenaufruf)
  • Nachrichtenzustellung durch Ablaufumgebung sichergestellt

Elemente des Aktor-Modells

  • kein geteilter Zustand zwischen Aktoren
    • keine Synchronisierung erforderlich
  • jeder Aktor hat eigene Mailbox
    • eingehende Nachrichten
    • Nachrichten werden sequentiell bearbeitet
  • Nachrichten werden asynchron zugestellt
  • ein Aktor kann neue Aktoren erzeugen
    • Verteilung von Teilaufgaben

Apache Software Foundation (2021)
  • Aktor kann Nachricht nehmen und verarbeiten (verbrauchen)
  • oder unverändert weiterleiten
  • Alternativ: Verarbeitung = neue Nachricht(en) erzeugen und weiterleiten
  • Aktor kann Nachrichten an sich selber senden

Nachrichten

  • Aktor interagieren durch Nachrichtenaustausch
  • Nachrichten werden asynchron zugestellt
    • empfangender Aktor blockiert nicht bis zum Empfang
  • Mailbox \(\neq\) Channel
    • mehrere Produzenten für eine Mailbox möglich
    • ein Empfänger behandelt die Nachrichten mehrere anderer Aktoren, die in seiner Mailbox ankommen, synchron
    • “put” und “take” sind atomare Operationen
  • Nachrichten immutable
    • Aktor kann Nachricht nehmen und verarbeiten (verbrauchen)
    • oder unverändert weiterleiten
    • Alternativ: Verarbeitung = neue Nachricht(en) erzeugen und weiterleiten
  • Aktor kann Nachrichten an sich selber senden

Aktoren \(\neq\) Threads

  • Die Anzahl der Aktoren kann größer sein, als die Zahl der sinnvoll zu behandelnden Threads (wie bei Fibers, Go-Coroutinen etc.)
  • Es gibt eine Komponente im Aktor-System (“Dispatcher”), die eine Zuordnung von Aktor zu Thread (z.B. aus einem Threadpool) übernimmt.

Aktoren überwachen Aktoren

Aktor-Programme und Fehler

Aktoren überwachen andere Aktoren (solche “Überwacher” heißen supersivor).

Statt “defensive programming” oder Exception Handling: “let it crash”:

  • Worker (Aktor, der konkrete Anwendungsaufgabe umsetzt) ist nur für Verarbeitung korrekter Eingabeparameter gemacht.
  • Fehlerbehandlung wird in den Supervisor verschoben.

Vorteile

  • Programme sind einfacher (Fehlerbehandlung ausgelagert).
  • Da Aktoren getrennt sind und keine Variablen teilen, besteht keine Gefahr, dass mehr als ein Programmteil abstürzt (Worker stürzt ab, Supervisor bleibt).
  • Supervisor kann weitergehende Fehlerbehandlungsstrategien auch für unvorhergesehene Situationen implementieren und zentrale Protokollierung vornehmen.

Hierarchie von Error Kernels

Aktoren (Supervisor) überwachen Aktoren (Worker oder Supervisor)

  • sie werden über Abstürze informiert
  • Ersatz für Execption Handling
  • sehr robust

Butcher (2014, 135)

Design Prinzipien für Aktoren

  • Ein Aktor sollte nur eine klar abgegrenzte Aufgabe haben (“single responsibility”-Prinzip)
  • Spezifische Supervisor: Ein Supervisor sollte nur genau eine Art von Worker überwachen.
  • Einfacher Error Kernel: Die Wurzel in der Überwachungshierarchie sollte so einfach wie möglich sein, um Fehler zu vermeiden, die nicht abgefangen werden können.
  • Fehler-Zonen: Fehler sollten sich nur auf einen abgegrenzten Teil der Hierarchie von Supervisors/Workers auswirken.

Akka (Java API)

Auf Nachrichten reagieren

3record Greeting(String from) {}

1public class DemoMessagesActor extends AbstractLoggingActor {
2    @Override
    public Receive createReceive() {
4        return receiveBuilder()
            .match(Greeting.class, msg -> {
                log().info("greeted by {}", msg.from());
            })
          .build();
    }
}
1
Actor erweitert AbstractLoggingActor oder AbstractActor (ohne Logging)
2
createReceive()-Methode wird überschrieben
3
reagiert auf Nachrichten vom Typ Greeting
4
“Builder”-Design Pattern für Receive-Objekt

Auf Nachrichten reagieren

public class NewActor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Msg1.class, this::receiveMsg1)
            .match(Msg2.class, this::receiveMsg2)
            .match(Msg3.class, this::receiveMsg3)
            .build();
        }
    private void receiveMsg1(Msg1 msg) {...}
    private void receiveMsg2(Msg2 msg) {...}
    private void receiveMsg3(Msg3 msg) {...}
}

Aktor programmatisch erzeugen

ActorRef parent =
    Akka.system().actorOf(Props.create(MyActor.class), "myactor");
  • ActorRef ist der Basis-Typ für Aktoren in Akka
  • actorOf ist Factory-Methode zum Erzeugen neuer Aktoren
    • Aktoren werden immer in einem (“Eltern”-) Kontext erzeugt
    • Akka.system() ist “Wurzel”-Kontext
    • Auswahl des Aktor-Typs durch Props.create(): hier Implementierungsklasse des Aktors angeben – z. B. MyActor
    • optional kann als zweiter Parameter von actorOf ein Akka-interner Name angegeben werden, über den der Aktor identifiziert werden kann

Kind-Aktor in Eltern-Aktor erzeugen

ActorRef child =
    getContext().actorOf(Props.create(SubActor.class), "sub");

Aktor-Hierarchie

Aktor-Hierarchie

https://doc.akka.io/docs/akka/current/general/ActorPath.png (abgewandelt)

Nachrichtenversand

target.tell("Hello", getSelf());
getSender().tell(new Messages.ResultMsg(result), getSelf());
  • getSender(): von welchem Aktor kam die letzte Nachricht (die gerade behandelt wird)?

Nachrichtenversand

import static akka.pattern.PatternsCS.ask;

CompletableFuture<Object> future1 = ask(actorA, "request 1", 1000);
CompletableFuture<Object> future2 = ask(actorB, "request 2", 1000);
CompletableFuture<Result> transformed =
CompletableFuture.allOf(future1, future2).thenApply(v -> {
    String x = (String) future1.join();
    String s = (String) future2.join();
    return new Result(x, s);
});
  • Niemals ein CompletableFuture als Nachricht versenden: Wenn Aktoren auf unterschiedlichen Rechnern sind, müsste das Future zum Versand serialisiert werden, was nicht funktioniert.

Design-Prinzipien für Nachrichtenversand

  • “Fire-and-Forget” statt “Request-Reply”: Nachrichten sollten tendenziell eher nicht auf Aufforderung versendet werden, sondern aufgrund eines Ereignisses/situativen Zustands.
  • tell vor ask bevorzugen: mit tell kann genauso ein Request-Reply Kommunikationsmuster umgesetzt werden: In vielen Fällen ist es nicht erforderlich eine Korrelation zwischen Anfrage und Antwort herzustellen.
    • besser: Frage in Antwort wiederholen (bzw. wesentliche Elemente daraus), dann kann der Empfänger sie unabhängig von seiner Anfrage interpretieren
    • ermöglicht auch das Antworten ans andere Aktoren als den ursprünglichen Anfrager

Monitoring

public class WatchActor extends AbstractActor {
    ActorRef child = getContext().actorOf(
            Props.create(ListenerActor.class), "listener");
    ActorRef lastSender;
    public WatchActor() {
        getContext().watch(this.child);
    }
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(PoisonPill.class, (msg) -> {
                getContext().stop(this.child);
                this.lastSender = getSender(); })
            .match(Terminated.class, (msg) -> {
                if (msg.getActor() == this.child) {
                    this.lastSender.tell("finished", null);
                } })
            .build();
    }
}

Parallele Suche eines Suchstrings in einigen wenigen Textdateien

Hettel und Tran (2016)

Parallele Suche eines Suchstrings in einigen wenigen Textdateien

  • Master erzeugt Listener
  • FindMsg mit drei zu durchsuchenden Dateien trifft beim Master ein
  • Master erzeugt jeweils Worker und sendet eine WorkMsg an ihn
  • Worker durchsucht Datei aus WorkMsg und sendet Ergebnisse als ResultMsg an Master zurück
  • Master sammelt alle Antworten, erzeugt daraus eine Gesamt-ResultMsg und schickt sie an den Listener
  • Listener gibt Gesamtergebnis auf Konsole aus und terminiert Anwendung

Laboraufgabe “Parallele Suche eines Suchstrings in einigen wenigen Textdateien”

  • Projekt: pp.11.01-FindWords
  • Bearbeitungszeit: 25 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 16

Router

Abgrenzung Router zu Aktor

  • Router ähnlich zu Aktoren
  • kein eigener Zustand und Verhalten
  • Eingang für eine Reihe nachgeordneter Aktoren (“Routees”)

Routee

  • in der Regel gleichartig (z. B. von derselben Klasse)
  • verarbeiten prinzipiell dieselben Nachrichtentypen
  • Router kennt seine Routees

Nachrichtenversand über Router

  • Nachricht an Routee wird nicht direkt geschickt, sondern über Router (“route”)
  • Router ändert Nachricht nicht, Absender bleibt ursprünglicher Sender
  • Router entscheidet aufgrund Entscheidungslogik, an welchen Routee weitergeleitet wird
  • Router dient als “Load-Balancer” für Routees

Round-Robin Routing

akka.routing.*
    >> RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic






Allen (2013, 4)

Random Routing

akka.routing.*
       RoundRobinRoutingLogic
    >> RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic






Allen (2013, 3)

Smallest Mailbox Routing

akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
    >> SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic






Allen (2013, 5)

Broadcast Routing

akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
    >> BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic






Allen (2013, 6)

Scatter Gather First Completed Routing

akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
    >> ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic





Allen (2013, 7)

Router-Verwendung

var routees = new ArrayList<Routee>();
for (var i = 0; i < 5; i++) {
    var routee = getContext().actorOf(Props.create(Worker.class));
    getContext().watch(routee);
    routees.add(new ActorRefRoutee(routee));
}
var router = new Router(new RoundRobinRoutingLogic(), routees);

// ...
router.route(message, getSelf());

Parallele Suche eines Suchstrings in sehr vielen Textdateien mit einem Router

Hettel und Tran (2016)

Parallele Suche eines Suchstrings in sehr vielen Textdateien mit einem Router

  • Master erzeugt am Anfang Listener und alle Worker
  • Master erzeugt dann einen Router, dem die Worker als Routees übergeben werden
  • eine FindMsg kommt beim Master an
  • die drei darin enthaltenen Dateien werden vom Master als WorkMsg an den Router geschickt.
  • Router sendet erste und zweite WorkMsg an w1 und w2
  • die dritte WorkMsg geht vom Router wieder wegen Round Robin-Logik an w1
  • Master sammelt drei ResultMsg ein und sendet Gesamtergebnis als ResultMsg an Listener

Laboraufgabe “Parallele Suche eines Suchstrings in sehr vielen Textdateien mit einem Router”

  • Projekt: pp.11.02-RouterFindWords
  • Bearbeitungszeit: 35 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 16

Kopplung von Router und Routees

Gruppe

Im Fall einer Router-Group sind Router und Routees weniger eng gekoppelt:

  • Der Router erzeugt “seine” Routees-Aktoren nicht selber, sondern bekommt sie mitgeteilt.
  • Er überwacht die Routees nicht selber.

Pool

Im Fall eines Router-Pools sind Router und Routees enger gekoppelt als bei einer Gruppe:

  • Der Router erzeugt “seine” Routees-Aktoren selber und überwacht sie.
  • Sollte ein Routee terminieren, entfernt der Router sie aus seiner Routee-Liste.
ActorSystem sys = ActorSystem.create("sys");
ActorRef r =  sys.actorOf(
      new RoundRobinPool(5).props(Props.create(Worker.class)), "router");

Referenzen

Allen, Jamie. 2013. Effective Akka. Sebastopol: O’Reilly.
Apache Software Foundation. 2021. Example Network of Several Actors (Apache Flink Confluence Site: Akka and Actors). 15. März. https://cwiki.apache.org/confluence/download/attachments/53741538/actorNetwork.png (zugegriffen: 20. Dezember 2021).
Butcher, Paul. 2014. Seven Concurrency Models in Seven Weeks. When Threads Unravel. Dallas: The Pragmatic Programmers.
Hettel, Jörg und Manh Tien Tran. 2016. Nebenläufige Programmierung mit Java. Konzepte und Programmiermodelle für Multicore-Systeme. Heildelberg: dpunkt.verlag.
Hewitt, Carl, Peter Bishop und Richard Steiger. 1973. A Universal Modular ACTOR Formalism for Artificial Intelligence. In: Proceedings of the Third International Joint Conference on Artificial Intelligence, 235–245. Stanford, 20–23. August. http://www.ijcai.org/Proceedings/73/Papers/027B.pdf (zugegriffen: 20. Dezember 2021).