scalatutorial.de

Stufe 4Funktional denken · Lektion 24 von 34

Nebenläufigkeit: Futures und Aktoren

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 6/6 dieser Stufe

Mehrere Aufgaben gleichzeitig zu erledigen ist auf der JVM Alltag – und eine klassische Fehlerquelle. Scala bietet mit Future einen Weg, nebenläufige Berechnungen als Werte zu behandeln und wie Option oder Try zu verketten, ohne sich mit Threads und Sperren herumzuschlagen.

Warum nicht einfach Threads?

Threads direkt zu verwalten ist mühsam und fehleranfällig. Sobald mehrere Threads dieselben veränderlichen Daten lesen und schreiben, drohen Race Conditions: Das Ergebnis hängt davon ab, welcher Thread zufällig zuerst dran war. Sperren (synchronized, Locks) helfen, bringen aber neue Probleme wie Verklemmungen. Scalas Antwort besteht aus zwei Zutaten, die Sie schon kennen: unveränderliche Daten und Berechnungen als Werte.

Ein erstes Future

 Scala 3
import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.*

def langsam(n: Int): Int =
  Thread.sleep(1000)
  n * n

val f: Future[Int] = Future(langsam(7))
println("Future gestartet, das Programm läuft weiter …")

Future(…) startet die Berechnung im Hintergrund und gibt sofort ein Future[Int] zurück – ein Platzhalter für ein Ergebnis, das später vorliegt. Wo es ausgeführt wird, bestimmt der ExecutionContext, im Beispiel ein globaler Thread-Pool.

Ergebnisse verknüpfen statt warten

Die naheliegende Idee, einfach auf das Ergebnis zu warten, blockiert einen Thread und macht den Vorteil zunichte. Besser: Man beschreibt, was mit dem Ergebnis passieren soll, wenn es da ist.

 Scala 3
val text: Future[String] = f.map(q => s"Das Quadrat ist $q")

Future ist eine Monade – map, flatMap und for-Ausdrücke funktionieren wie gewohnt. Das macht es leicht, mehrere Aufgaben parallel laufen zu lassen und am Ende zusammenzuführen:

Drei Futures laufen parallel und werden am Ende zusammengeführtHaupt-ThreadFuture AFuture BFuture Cdrei Aufgaben laufen parallel; die Verknüpfung wartet auf die langsamste
Drei Futures laufen parallel, das Ergebnis steht fest, sobald das langsamste fertig ist.
 Scala 3
def preis(artikel: String): Future[BigDecimal] = Future { Thread.sleep(800); BigDecimal(20) }
def lager(artikel: String): Future[Int]        = Future { Thread.sleep(500); 12 }
def versand(plz: String): Future[BigDecimal]   = Future { Thread.sleep(900); BigDecimal("4.90") }

// Erst starten, dann verknüpfen – so laufen alle drei gleichzeitig
val fp = preis("Buch")
val fl = lager("Buch")
val fv = versand("90574")

val angebot: Future[String] =
  for
    p <- fp
    l <- fl
    v <- fv
  yield s"Preis ${p + v} € inkl. Versand, $l Stück auf Lager"

Fehler in Futures

Wirft die Berechnung eine Exception, endet das Future als Fehlschlag. Intern enthält es dann ein Failure – dieselbe Klasse wie bei Try:

 Scala 3
val kaputt = Future(10 / 0)

kaputt.recover { case _: ArithmeticException => 0 }   // Future mit 0
kaputt.onComplete {
  case scala.util.Success(n) => println(s"Ergebnis $n")
  case scala.util.Failure(e) => println(s"Fehler: ${e.getMessage}")
}

Warten – nur am Rand

In einem kleinen Programm oder Test muss man irgendwann auf das Ergebnis warten, sonst endet main, bevor die Arbeit getan ist. Dafür gibt es Await:

 Scala 3
val ergebnis = Await.result(angebot, 5.seconds)
println(ergebnis)

In Server-Anwendungen gehört Await nicht in den Programmkern: Webframeworks wie Play nehmen ein Future direkt entgegen und schicken die Antwort, sobald es fertig ist.

Viele Futures auf einmal

 Scala 3
val ids = List(1, 2, 3, 4, 5)
val alle: Future[List[Int]] = Future.traverse(ids)(id => Future(langsam(id)))

Future.traverse startet für jedes Element ein Future und sammelt die Ergebnisse in einer Liste. Future.sequence macht aus einer List[Future[A]] ein Future[List[A]].

Aktoren: ein anderes Modell

Futures eignen sich für einzelne Berechnungen, die ein Ergebnis liefern. Für langlebige Komponenten mit eigenem Zustand – ein Warenkorb, eine Chat-Sitzung, ein Gerät im Netz – gibt es das Aktorenmodell: Jeder Aktor hat einen privaten Zustand und ein Postfach, verarbeitet eine Nachricht nach der anderen und spricht mit anderen Aktoren nur über Nachrichten.

Zwei Aktoren mit eigenem Postfach tauschen Nachrichten ausAktor KassePostfach ▢ ▢ ▢Zustand: privatAktor LagerPostfach ▢ ▢ ▢Zustand: privatReserviere(3)ReserviertKein geteilter Speicher: Aktoren sprechen nur über Nachrichten miteinander.
Aktoren teilen keinen Speicher, sie tauschen Nachrichten aus.

In der Scala-Welt setzen Akka und dessen freier Ableger Apache Pekko dieses Modell um. Wie das aussieht, zeigt die Lektion über Akka und Pekko.

Passt dazu