CompletableFuture 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

=> Grundlage: Hettel und Tran (2016, Kap. 15)

  • Monte-Carlo-Berechnung von \(\pi\)
    • sequentiell und parallel mit Future
  • CompletableFuture (Framework)
    • lineare Verarbeitungsketten und Verzweigen/Vereinen
    • Synchronisations-Barrieren
    • Fehlerbehandlung in asynchronen Ketten
  • Monte-Carlo-Berechnung von \(\pi\)
    • parallel mit CompletableFuture

Monte-Carlo-Berechnung von \(\pi\)

Monte-Carlo-Berechnung von \(\pi\)

  • zufällig gewählte Punkte mit Koordinaten ([0..1], [0..1])
  • man kann leicht feststellen, ob Sie genau auf, innerhalb oder außerhalb des Einheitsviertelkreis (\(r=1\)) liegen

Monte-Carlo-Berechnung von \(\pi\)

  • sehr viele Punkte (x, y) mit gleichmäßiger Verteilung
  • das Verhältnis der Anzahl der Punkte innerhalb des Viertelkreises zu allen innerhalb des Quadrats entspricht dem Verhältnis der Fläche des Viertelkreises (Radius 1) zur Fläche des Quadrats (Kantenlänge 1)

\[\frac{\mbox{Anzahl der Punkte innerhalb und auf dem Kreis}}{\mbox{Anzahl aller Punkte}} = \frac{\frac{\pi r^2}{4}}{r^2}\]

  • da \(r=1\), kann \(\pi\) angenähert werden als: pi = 4.0 * in / (in + out)
    • in: Anzahl der Punkte innerhalb des Kreises (sqrt(x*x + y*y) <= 1)
    • out: die restlichen, die außerhalb des Kreises liegen (sqrt(x*x + y*y) > 1)

Laboraufgabe “Monte-Carlo-Algorithmus zur Annäherung von \(\pi\) – sequenziell”

  • Projekt: pp.08.01-ConcurrencyMonteCarloPiSeq
  • Bearbeitungszeit: 5 Minuten
  • Musterlösung: 5 Minuten
  • Kompatibilität: mindestens Java SE 16

Laboraufgabe “Monte-Carlo-Algorithmus zur Annäherung von \(\pi\) – mit Future

  • Projekt: pp.08.02-ConcurrencyMonteCarloPiFuture
  • Bearbeitungszeit: 15 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 16

Überblick und Grundlagen von CompletableFuture

Laboraufgabe commonPool

  • Projekt: pp.08.03-CommonPool
  • Bearbeitungszeit: 10 Minuten
  • Musterlösung: 10 Minuten
  • Kompatibilität: mindestens Java SE 10

Zweck von CompletableFuture

  • Pipelining: funktionale Verkettung mehrerer Future/Callable
    • Output eines Callable wird Input für nächstes Callable
    • verwendet Regeln, um zu entscheiden, wo nächstes Callable ausgeführt wird:
      • im vorigen Thread
      • im Aufrufer-Thread oder
      • einem neuen Thread
  • Verwendung des commonPool
  • asynchrone Methodenaufrufe
    • Standard für verteilte Architekturen
    • reaktive Programmierung / reaktive Architektur \(\to\) “hängt” nicht, (vertikal) skalierbar

Grundlegende Struktur von CompletableFuture

  • Future: read-only Container für zukünftiges Ergebnis (“Promise”)
  • CompletionStage: triggert weitere Verarbeitung

CompletionStage (Push-API)

  • bietet eine Reihe von Dreier-Paaren:
    • “nicht-asynchrone”
      Ausführung
    • asynchrone Ausführung in
      commonPool
    • asynchrone Ausführung
      mit eigenem Executor
      statt commonPool
  • Ergebnis ist immer eine neue
    CompletionStage

CompletionStage (Push-API)

Überblick über das API von CompletableFuture

nach Hettel und Tran (2016, 240) (Änderungen: Annotationen und supplyAsync statt applyAsync in Start-API)

internes API

static CompletableFuture<Integer> calculateAsync() {
    var result = new CompletableFuture<Integer>();
    ForkJoinPool.commonPool().submit(() -> {
        try {
            var res = /* aufwändige Berechnung, Ergebnis z.B. */ 42;
            result.complete(res);
        } catch (Exception ex) {
            result.completeExceptionally(ex);
        }
    });
    return result;
}

public static void main(String... args) {
    var cf = calculateAsync();
    try {
        System.out.println(cf.get());
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } catch (ExecutionException e) {
        System.err.print(e);
    }
}

Start-API

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;

public class SimpleCompletableFuture {
    static class Task implements Supplier<Integer> {
        @Override
        public Integer get() {
            return /* aufwändige Berechnung, Ergebnis z.B. */ 42;
        }
    }

    public static void main(String... args)
            throws InterruptedException, ExecutionException {
        var future = CompletableFuture.supplyAsync(new Task());


        System.out.println(future.get());
    }
}

Start-API

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;


public class SimpleCompletableFuture {







    public static void main(String... args)
            throws InterruptedException, ExecutionException {
        var future = CompletableFuture.supplyAsync(() -> {
            return /* aufwändige Berechnung, Ergebnis z.B. */ 42;
        });
        System.out.println(future.get());
    }
}

Push-API von CompletableFuture

– lineare Ketten und Verzweigungen –

asynchrone Verarbeitungskette

CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> f)
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)

Für jedes thenApplyAsync und theAcceptAsync wird ggf. ein eigener Thread aus dem Threadpool benutzt.

Hettel und Tran (2016, 246)

asynchrone Verarbeitungskette

Hettel und Tran (2016, 252) (modifiziert)

Beispiel für eine asynchrone Verarbeitungskette

public class Service {
    static User getUser(int userId) { ... }
    static Profile getProfile(User user) { ... }
    static AccessRight getAccessRight(Profile profile) { ... }
}

CompletableFuture<Void> cf = CompletableFuture
    .supplyAsync(() -> Service.getUser(42))
    .thenApplyAsync((user) -> Service.getProfile(user))
    .thenApplyAsync((profile) -> Service.getAccessRight(profile))
    .thenAcceptAsync((access) -> System.out.println(access));

cf.join(); // wartet auf das Ende der Berechnung von cf,
           // hier also auf die erfolgte Ausgabe

Beispiel für eine asynchrone Verarbeitungskette

public class Service {
    public static User getUser(int userId) { ... }
    public static Profile getProfile(User user) { ... }
    public static AccessRight getAccessRight(Profile profile) { ... }
}

CompletableFuture<Void> cf = CompletableFuture
    .supplyAsync(() -> T)
    .thenApplyAsync((T t) -> U)
    .thenApplyAsync((U u) -> V)
    .thenAcceptAsync((V v) -> Void); // void (kleingeschrieben)
      // ist in Java kein Typ, sondern ein Schlüsselwort in Signaturen
cf.join();

“nicht-asynchrone” Verarbeitungsschritte

Methode Task-Typ Resultat
thenRun Runnable: () -> void CompletableFuture<Void>
thenAccept Consumer: (T) -> void CompletableFuture<Void>
thenApply Function: (T) -> U CompletableFuture<U>
  • Falls der “vorige” CompletableFuture-Task schon fertig ist, wird der nächste Task im Aufrufer-Thread ausgeführt.
  • Falls der “vorige” CompletableFuture-Task noch nicht fertig ist, wird der nächste Task anschließend im Thread des vorigen Tasks ausgeführt.

Split-Pattern

var task1 = CompletableFuture.supplyAsync(() -> "4711");
var task2 = task1.thenApplyAsync((in) -> in.length());
var task3 = task1.thenApplyAsync((in) -> in.equals("42"));


  • T: String
    • '4711'
  • U: Integer
    • 4
  • V: Boolean
    • false

Hettel und Tran (2016, 252)

Vereinen von Verarbeitungsketten

Hettel und Tran (2016, 252)

Methoden zur Vereinigung von Abläufen. “CF” steht für CompletableFuture und “CS” für CompletionStage

Zusammenführen durch thenCombine (Verrechnen)

var task1 = CompletableFuture.supplyAsync(() -> 47);
var task2 = CompletableFuture.supplyAsync(() -> "11");
var task3 = task1.thenCombineAsync(task2, (n, s)->String.valueOf(n)+s);


  • T: Integer
    • 47
  • U: String
    • '11'
  • V: String
    • '4711'

Hettel und Tran (2016, 253)

Zusammenführen durch applyToEither (“ODER”)

var task1 = CompletableFuture.supplyAsync(() -> 47);
var task2 = CompletableFuture.supplyAsync(() -> 11);
var task3 = task1.applyToEitherAsync(task2, (n) -> n > 20);


  • T: Integer
    • 47
  • U: Integer
    • 11
  • V: Boolean
    • true oder false

Hettel und Tran (2016, 255)

Barrieren und Zusammenführen

Hettel und Tran (2016, 257) (modifiziert)

Barrieren und Zusammenführen

CompletableFuture.allOf(     // alle müssen beendet werden
    CompletableFuture.runAsync( () -> { /*...*/ } ),
    CompletableFuture.runAsync( () -> { /*...*/ } ),
    CompletableFuture.runAsync( () -> { /*...*/ } )
).thenAccept((Void) -> System.out.println("done") );


CompletableFuture.anyOf(    // das frühest fertige
    CompletableFuture.supplyAsync( () -> { /*...*/ } ),
    CompletableFuture.supplyAsync( () -> { /*...*/ } ),
    CompletableFuture.supplyAsync( () -> { /*...*/ } )
).thenAccept((first) -> System.out.println(first));

Fehlerbehandlung und Abbruch

var cf = CompletableFuture
    .supplyAsync(() -> 42)
    .thenApplyAsync(r -> r / 0)
    .thenApplyAsync(r -> r * r)
    .thenApplyAsync(r -> r > 0)
    .handle((r, th) -> {
        if (r != null) {
            System.out.println("Resultat: " + r);
            return r;
        } else {
            System.err.println("error: " + th);
            return false;
        }
    });
System.out.println(cf.join());

Hettel und Tran (2016, 258) (modifiziert)

Fehlerbehandlung und Abbruch

var cf = CompletableFuture
    .supplyAsync(() -> 42)
    .thenApplyAsync(r -> r / 0)
    .thenApplyAsync(r -> r * r)
    .thenApplyAsync(r -> r > 0)
    .whenComplete((r, th) -> {
        if (r == null) {
            System.err.println("error: " + th);
        }
    });
cf.join();

Hettel und Tran (2016, 258) (modifiziert)

Monte-Carlo-Berechnung von \(\pi\)

Laboraufgabe “Monte-Carlo-Algorithmus zur Annäherung von \(\pi\) – mit CompletableFuture

  • Projekt: pp.08.04-ConcurrencyMonteCarloPiCF
  • Bearbeitungszeit: 15 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 16

Referenzen

Hettel, Jörg und Manh Tien Tran. 2016. Nebenläufige Programmierung mit Java. Konzepte und Programmiermodelle für Multicore-Systeme. Heildelberg: dpunkt.verlag.