Scala Futures und Promises in der Praxis

Sicher in die Zukunft

Sicher in die Zukunft

Scala Futures und Promises in der Praxis

Sicher in die Zukunft


Während Java gerade erst in Version 8 ein relativ leichtgewichtiges API zur asynchronen Ausführung voneinander abhängiger Tasks erhalten hat, gibt es solche Möglichkeiten woanders schon seit geraumer Zeit. Scala ermöglicht es, mithilfe von Futures und Promises asynchrone Operationen zu erstellen, zu verknüpfen und die Ergebnisse weiterzuverarbeiten. Durch erweiterte Möglichkeiten im Bereich Exception Handling ist das API auch für den Alltagseinsatz jenseits der kleinen vorgestellten Artikelbeispiele tauglich.

Multi-Threading ist in Java seit jeher ein schwieriges und fehleranfälliges Thema. Neben der inhärenten Komplexität des Themengebiets gab es bislang kein API, das Entwicklern ein angenehmes Abstraktionsniveau bot. Erschwerend kommt hinzu, dass die Korrektheit von Multi-Threading-Code nicht einfach in Unit-Tests zu überprüfen ist. So ist es nicht verwunderlich, dass bislang oft versucht wurde, das Handling von mehreren Threads zu vermeiden. Und obwohl Oracle das Java-Concurrent-API immer wieder erweitert und weiterentwickelt hat, bieten erst die neuesten Ergänzungen in Java 8 mit CompletableFuture die notwendige Simplizität, um wirklich einfach die Abläufe von mehreren asynchronen Threads programmatisch zu steuern. Die länger vorhandenen API-Features wie Futures und ExecutorServices, ForkJoinPools oder gar Locks und Semaphores bieten zwar alle benötigten Grundlagen und könnten auch direkt verwendet werden, haben aber in den meisten Fällen ein zu niedriges Abstraktionsniveau für eine einfache Anwendung. Das niedrige Abstraktionsniveau macht es Anfängern in dem Thema leicht, Fehler zu machen, und führt auch schnell zu viel Boilerplate-Code, der dem Clean-Code-Gedanken zuwiderläuft. Gerade im Vergleich mit anderen Sprachen muss sich Java den Vorwurf gefallen lassen, dieses Thema bisher etwas stiefmütterlich behandelt zu haben. So ist es in JavaScript-Applikationen spätestens seit der Standardisierung von Promises ein Leichtes, asynchronen Code zu entwickeln. Dennoch ist es für moderne Java-Applikationen sinnvoll, sich eingehender mit diesem Thema zu befassen, da moderne Prozessorarchitekturen nur unter Verwendung von Multi-Threading-Code optimal genutzt werden können.

Multi-Threading in Scala

Die Entwickler von Scala haben sich dieser Problematik angenommen und ein API entwickelt, mit dem asynchrone Aufgaben in einer nicht blockierenden Art gesteuert werden können. Die Basis dafür bildet das Fork/Join-API, das mit Java 7 eingeführt wurde. Es ist ideal, um eine Reihe von kurzen, nicht blockierenden Jobs zu starten und zu verwalten und wird z. B. auch vom Java-8-Streams-API verwendet. Dabei wird eine Reihe von Worker-Threads erstellt, die die ihnen zugewiesenen Jobs abarbeiten. Sobald ein Worker-Thread keine eigenen Jobs mehr hat, versucht er, sich Jobs von anderen Worker-Threads zu nehmen. Das Scala-Concurrent-API erstellt standardmäßig einen Worker-Thread pro CPU-Kern. Diese Konfiguration lässt sich aber nach Belieben anpassen. Im Folgenden wollen wir die beiden Kernelemente des Scala-Concurrent-API vorstellen: Future und Promise. Konzeptuell kann man sich beide Objekte als Container für Werte vorstellen. Diese Werte existieren zwar noch nicht, wenn man die Container erstellt, werden aber zu einem späteren Zeitpunkt bekannt sein.

Bei der Beschreibung des API gilt es zu beachten, dass Scala im Gegensatz zu Java auch Call-by-Name-Aufrufe kennt. Bei Call-by-Name-Aufrufen wird der übergebene Parameter nicht sofort ausgewertet, sondern erst, sobald er in der Methode zum ersten Mal benutzt wird. So ist es auch möglich, dass der Parameter nie ausgewertet wird. Das ist z. B. der Fall, wenn der Parameter nur in einem If-Block benutzt wird, dessen If-Bedingung nicht zutrifft. Um diese Call-by-Name-Parameter deutlich zu machen, werden sie in Beispielen mittels geschweifter Klammern gekennzeichnet.

Ein Blick in die Zukunft

Das wichtigste Element des Scala-Concurrent-API ist das Future. Ein Future kann als lesbarer Container für ein zukünftiges Ergebnis verstanden werden. Um ein Future zu erstellen, übergibt man dem Konstruktor einen Lambdaausdruck. Genaugenommen handelt es sich dabei um eine Scala-Funktion. Die asynchrone Ausführung dieses Ausdrucks wird dann sofort angestoßen. Allerdings hat man keinerlei Möglichkeit, auf das Scheduling Einfluss zu nehmen. Man kann Futures weder priorisieren noch pausieren oder gar abbrechen. Allerdings kann man das Resultat eines Futures abfragen beziehungsweise auf das Resultat reagieren. Der Zustand eines Futures lässt sich mittels...