© Excellent backgrounds/Shutterstock.com
Teil 1: Die Grundlagen von Akka

Reaktive Sudokus


Um moderne Hardware effizient zu nutzen, müssen Entwickler sie möglichst so aufbauen, dass sie mit parallelen Prozessen zurechtkommt. Und das auch noch möglichst effizient. Das ist leider einfacher gesagt als getan, sind unsere Entwicklerhirne doch eher auf sequenzielle Abläufe ausgerichtet. Das Framework Akka tritt an, die Entwicklung paralleler Anwendungen zu erleichtern.

Video: Reaktive Programmierung für Dummys

Java brachte bereits in der ersten Version Unterstützung für Programme, die in mehreren Threads ablaufen können. Diese kam in Form von Schlüsselwörtern (wie synchronized oder volatile), Klassen der Standardbibliothek (z. B. Thread oder ThreadGroup) oder Methoden in Object (wie wait oder notify). Wer jedoch bereits versucht hat, mit diesen Bordmitteln komplexe nebenläufige Logik zu orchestrieren, der weiß, dass sie auf einem sehr niedrigen Abstraktionsniveau angesiedelt sind. Daher ist es schwierig, das korrekte Maß an Synchronisierung zu finden: Gerade ausreichend, um die Integrität der Daten sicherzustellen und den teilweise subtilen Anforderungen des Java-Memory-Modells zu genügen, aber auch nicht mehr als nötig, um die vorhandene Rechenleistung nicht auszubremsen.

Seit Java 5 hat sich die Situation etwas gebessert: Hier kam das util.concurrent-Paket hinzu mit speziell für parallele Anwendungsfälle optimierten Datenstrukturen (z. B. ConcurrentMap oder Queues) und Unterstützung für Threadpools und Synchronisationsprimitiva auf einem höheren Niveau (z. B. CountdownLatch oder Semaphore). Der grundlegende Ansatz zur Nebenläufigkeit ist aber gleich geblieben: Mehrere Threads greifen koordiniert auf gemeinsam genutzte Daten zu. Dies bringt einige Nachteile mit sich.

Es handelt sich dabei um einen dezentralen Ansatz. Jeglicher Datenzugriff muss entsprechend abgesichert sein. Das macht es schwierig, die Korrektheit der Anwendung zu verifizieren. Selbst wenn die Zugriffslogik auf Daten in einer Klasse gekapselt ist, kann eine einzige unbedachte Änderung genügen, um die Integrität der Daten zu korrumpieren. Mit Unit-Tests allein kann man sich dagegen oft nur schwer absichern.

Ein anderes Problem ist eher prinzipieller Natur: Die gemeinsame Nutzung von Daten durch mehrere Threads impliziert, dass manche Threads mitunter auf andere warten müssen, um ihre Operationen ungestört ausführen zu können. Nun sind Threads aber relativ teure Ressourcen und eigentlich zu schade dafür, untätig auf einen freien Zeitpunkt zum Datenzugriff zu warten. Zumal das Parken und spätere Reaktivieren eines Threads mit einem gewissen Overhead verbunden ist. Idealerweise möchte man die zu erledigenden Aufgaben auf eine überschaubare Menge von Threads verteilen, die dann ohne Unterbrechung laufen und laufen wie ein bekanntes Automodell aus der Prä-Diesel-Skandal-Ära.

Aktoren als Alternative

Um die beschriebenen Nachteile zu umgehen, wurden alternative Modelle entwickelt. Eins davon ist das bereits 1973 von Carl Hewitt und Kollegen vorgestellte Aktorenmodell [1]. Dabei geht es um Objekte, so genannte Aktoren, die eigene Daten halten können, diese aber nicht mit der Außenwelt teilen. Stattdessen kommunizieren sie mit ihrer Umgebung lediglich durch das Senden und Empfangen asynchroner Nachrichten. Zu diesem Zweck besitzen sie eine Mailbox, in der ankommende Nachrichten gespeichert und der Reihe nach abgearbeitet werden.

Aktoren existieren nicht im luftleeren Raum, sondern werden von einem Aktorensystem verwaltet. Dessen primäre Aufgabe besteht darin, einen Pool von Threads zu koordinieren und diesen den Aktoren zur Verarbeitung der Nachrichten in ihrer Mailbox zur Verfügung zu stellen. Dabei stellt das Aktorensystem sicher, dass zu einem gegebenen Zeitpunkt maximal ein Thread den Code eines Aktors ausführt (Abb. 1). Bei der Implementierung der Verarbeitungslogik braucht sich der Entwickler daher keine Gedanken über Threadsynchronisierung zu machen; das Aktorensystem garantiert, dass der Zugriff auf die Daten des Aktors exklusiv ist.

heger_akka_1.tif_fmt1.jpgAbb. 1: Zuteilung aktiver Threads auf Aktoren in einem Aktorensystem

Im Gegensatz zu Threads sind Aktoren keine teuren Ressourcen. Es handelt sich vielmehr um einfache Java-Objekte. Daher ist es auch unproblematisch, bei Bedarf eine größere Zahl davon zu erzeugen – durchaus auch dynamisch und temporär. Das Konzept sieht sogar vor, möglichst große Teile der Geschäftslogik als Aktoren zu modellieren. Je mehr Aktoren mit gefüllter Mailbox vorhanden sind, desto besser kann das Aktorensystem die verfügbare Rechenleistung verteilen. Auf diese Weise ergibt sich ganz natürlich eine Parallelisierung der Datenverarbeitung.

Das Framework Akka [2] bietet eine effiziente und vollständige Implementierung des Aktorenmodells. Es ist Open Source unter der Apache-2.0-Lizenz und lässt sich daher bedenkenlos auch in kommerziellen Projekten einsetzen. Hinter dem Framework steht die Firma Lightbend (ehemals Typesafe), die auch federführend in der Entwicklung von Scala ist. Daher ist es nicht verwunderlich, dass das Framework in dieser Programmiersprache geschrieben wurde. Neben der nativen Schnittstelle für Scala gibt es auch eine Anbindung für Java. Das Scala-Äquivalent ist allerdings eleganter und einfacher zu nutzen. Scalas weiterführende Fähigkeiten im Bereich API-Design haben die Akka-Entwickler konsequent eingesetzt. Die folgenden Beschreibungen beziehen sich daher auf diese Variante der Schnittstelle.

Hello Aktor

Nach diesen theoretischen Vorbetrachtungen wird es Zeit, sich eine konkrete Implementierung eines einfachen Aktors anzusehen. Listing 1 zeigt den obligatorischen Hello-World-Aktor.

Listing 1: „HelloActor.scala“

package de.javamagazin.akka.hello import akka.actor.Actor import de.javamagazin.akka.hello.HelloActor.GreetRequest object HelloActor { case class GreetRequest(name: String) } class HelloActor(greeting: String) extends Actor { override def receive: Receive = { case GreetRequest(name) => println(greeting + name) } }

Ein Aktor ist eine Klasse, die den Basis-Trait Actor erweitern muss. Um eine vollständige Implementierung zu bieten, ist die receive-Funktion zu definieren. Diese liefert eine partielle Funktion zurück, welche die Logik zur Verarbeitung der eintreffenden Nachrichten enthält (Kasten: „Die ‚receive‘-Funktion“). Sie wird für jede empfangene Nachricht aufgerufen. Eine typische Implementierung wertet die aktuelle Nachricht aus und leitet entsprechende Aktionen ein. Dieses Minimalbeispiel reagiert nur auf eine Nachricht mit einer Aufforderung zu einer Begrüßung, indem es einen ...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang