Threadpools und asynchrone Methodenaufrufe

Parallele Programmierung (3IB)

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

 

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

Überblick

  • ExecutorService für asynchrone Methodenaufrufe
    • asynchroner Funktionsaufruf (Data Flow)
    • Callable, Future, ExecutorService
  • Threadpools
    • Executors-Factory: Fixed und Cached Threadpools
    • Threadpool terminieren (u.a. shutdown())
    • ScheduledExecutorService (u.a. schedule(...))
    • Thread Pool Dimensionierung
    • Exceptions in Runnable und Callable bei Threadpool Verwendung
    • Exkurs: CompletionService für voneinander unabhängige Tasks

Threads, die ein Ergebnis berechnen

Asynchroner Funktionsaufruf für “Data Flow”-Aufgaben

Reihenfolgen, in der die Ausdrücke berechnet werden können:

  • sequentiell
    • \(1+2\) ; \(3+4\) ; \(3+7\)
    • \(3+4\) ; \(1+2\) ; \(3+7\)
  • parallelisiert
    • \(1+2\) || \(3+4\) ; 3+7

Butcher (2014, 73)

Laboraufgabe “Thread für Runnable mit Rückgabewert”

  • Projekt: pp.04.01-RunnableReturn
  • Bearbeitungszeit: 30 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 10

ExecutorService für asynchrone Methodenaufrufe: Callable und Future

Problemstellung: asynchroner Methodenaufruf

Wie kann ein nebenläufiger Thread ein Ergebnis abliefern?

  • Die einzige Aufgabe solch eines Threads ist, genau eine (ggf. langdauernde) Berechnung durchführen.
  • Die Berechnung soll nebenläufig ablaufen: Nach dem Start geht es direkt im Programmfluss des Erzeugers weiter.
  • Man weiß nicht, wie lange der Thread für die Berechnung braucht. Eine Schnittstelle ist deshalb erforderlich um zu prüfen, ob das Ergebnis schon vorliegt.
  • Bei der Berechnung könnten Exceptions geworfen werden. Die sollen nicht asynchron durchgereicht werden.

Asynchroner Funktionsaufruf für “Data Flow”-Aufgaben

Reihenfolgen, in der die Ausdrücke berechnet werden können:

  • sequentiell
    • \(1+2\) ; \(3+4\) ; \(3+7\)
    • \(3+4\) ; \(1+2\) ; \(3+7\)
  • parallelisiert
    • \(1+2\) || \(3+4\) ; 3+7

Butcher (2014, 73)

Callable und Future

  • Asynchrone Verarbeitung wird in Java mit Callable, Runnable und Future gemacht.
  • Callable und Runnable repräsentieren die asynchron abzuarbeitende Aufgabe.
  • Mit Future kann das Ergebnis einer asynchronen Berechnung abgerufen werden (get())
  • FutureTask ist eine Implementierung von Future und Runnable.

ExecutorService-Framework

  • Die asynchrone Verarbeitung erfolgt in Java durch ExecutorService.
  • Implementierungen nehmen über die Methode submit Callable-Objekte an und starten call() asynchron.
  • Als Ergebnis bekommt der Aufrufer ein Future als Proxy, über den
    • auf das Ergebnis zugegriffen wird
    • die asynchrone Berechnung gemanagt wird

Callable asynchron mit ExecutorService ausführen

var e = /* demnächst */

var c = new Callable<V>(){
    public V call() {
        return /* ... */;
    }
};
var f = e.submit(c);


if(f.isDone()) {
    /* ... */
}

System.out.print(f.get());

Laboraufgabe “asynchrone Ausführung mit Callable und Future

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

Thread Pools

Warum Thread Pools?

  • Thread-Instanziierung ist “teurer” (dauert länger) als bei anderen Klassen, denn Datenstrukturen zur Threadkontrolle müssen angelegt werden und threadlokaler Stack-Speicher angefordert werden.
  • Optimierung: Thread-Objekte werden frühzeitig (z.B. beim Start) vorbereitet (“Thread Pool”). Wird ein Thread benötigt, wird einer der vorbereiteten Threads aus dem Pool genommen, mit einem Runnable-Objekt verbunden und (re-) aktiviert (statt start()).
  • Beim Ende der run()-Methode wird der Thread nicht vergessen und über die Garbage Collection entfernt, sondern deaktiviert und in den Thread Pool zur Wiederverwendung eingestellt.

ExecutorService-Framework

Die Klasse Executors stellt Factory-Methoden zur Erzeugung von Objekten zum ExecutorService-Interface bereit:

  • newCachedThreadPool()
  • newFixedThreadPool
    (nThreads: int)
  • newSingleThread
    Executor()

Shutdown eines Thread Pools

  • shutdown()
    • Die bereits eingestellten Aufgaben werden noch abgearbeitet, neue werden zurückgewiesen.
    • Der ExecutorService beginnt, herunterzufahren. Der Aufruf von shutdown() ist aber asynchron (es geht direkt im Anschluss weiter im Programmablauf.
  • isShutdown()
    • prüfen, ob der ExecutorService bereits fertig terminiert ist (nach shutdown()).
  • shutdownNow()
    • Erzwingen der sofortigen Terminierung der Threads im ExecutorService: Alle aktiven Tasks erhalten mit interrupt().

Das ExecutorService-Framework

var pool = Executors.newFixedThreadPool(5);
for (var i = 0; i <= 100; i++) {
    pool.submit(() ->
        System.out.println(
            Thread.currentThread().getName()));
}

pool.shutdown();
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-4
pool-1-thread-5
pool-1-thread-3
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-4
pool-1-thread-3
pool-1-thread-5
pool-1-thread-3
pool-1-thread-4
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-4

Thread Pool Framework von Java Executors Factory

Cached Thread Pool

erzeugt bei Bedarf neue Threads, unbenutzte Threads werden nach 60 Sekunden beendet

\(\to\) Programme mit kurzlebigen, asynchronen Aufgaben

Thread Pool Framework von Java Executors Factory

Fixed Thread Pool

genau nThreads werden erzeugt, überzählige Runnables bzw. Callables werden in Queue gespeichert

\(\to\) Programme mit sehr vielen unabhängigen Aufgaben

Thread Pool Framework von Java Executors Factory

Single Thread Pool

Sonderfall von newFixedThreadPool(1). Stürzt der Thread ab, wird er neu gestartet

\(\to\) Programme mit weniger unabhängigen Aufgaben; “Absturzsicherung”

Thread Pool Framework von Java Executors Factory

Scheduled Thread Pool

Aufgaben werden nach einer gegebenen Verzögerung bzw. periodisch ausgeführt

\(\to\) Programme mit vielen zeitlogisch abhängigen Aufgaben

Thread Pool Framework von Java Executors Factory

Single Scheduled Thread Pool

Sonderfall von newScheduledThreadPool(1). Stürzt der Thread ab, wird er neu gestartet

\(\to\) Programme mit einigen zeitlogisch abhängigen Aufgaben; “Absturzsicherung”

ScheduledExecutorService

  • Das Interface ScheduledExecutorService bzw. dessen Implementierung ScheduledThread
    PoolExecutor erlaubt Aufgaben in Form von Callable und Runnable
    • zu bestimmten Zeiten oder
    • wiederholt auszuführen.
  • Auch dieser ExecutorService wird durch die Factory Executor erzeugt.

ScheduledExecutorService

Dazu gibt es die Methoden:

  • schedule
    starte einmalig in \(x\) Zeiteinheiten
  • scheduleAtFixedRate
    warte \(x\) Zeiteinheiten und starte dann periodisch alle \(y\) Zeiteinheiten
  • scheduleWithFixedDelay
    warte \(x\) Zeiteinheiten und starte dann, nach dem Ende des Tasks warte \(y\) Zeiteinheiten und starte erneut, …

Beispiel ScheduledExecutorService

var scheduler = Executors.newScheduledThreadPool(1);
var beeperHandle = scheduler.scheduleAtFixedRate(
    () -> System.out.println("beep"), 3, 3, TimeUnit.SECONDS
);
scheduler.schedule(
    () -> beeperHandle.cancel(true), 5 * 3, TimeUnit.SECONDS
);
scheduler.schedule(
    () -> System.exit(0), (5 * 3) + 5, TimeUnit.SECONDS
);

Laboraufgabe “Thread Pools”

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

Heuristik zur Dimensionierung der Thread Pool Größe1

\[\begin{eqnarray} N_{CPU} &=& Anzahl~der~CPUs\\ &=& \mathtt{Runtime.getRuntime().availableProcessors()}\\ U_{CPU} &=& CPU~Auslastung~~~(0 < U_{CPU} \le 1)\\ \frac{W}{C} &=& Verh.~zwischen~Wartezeit~und~Rechenzeit\\ N_{Threads} &=& N_{CPU} \times U_{CPU} \times (1 + \frac{W}{C}) \end{eqnarray}\]

bei rechenintensiven Tasks (nie im BLOCKED-, WAITING- oder TIMED_WAITING-Zustand -> \(\frac{W}{C}=0\)) auf einem halb ausgelasteten System (\(U_{CPU}=\frac12\)):

\[\begin{eqnarray} N_{Threads} &=& N_{CPU} \times \frac12 \times (1 + 0) = N_{CPU}\\ &=& \mathtt{Runtime.getRuntime().availableProcessors()/2} \end{eqnarray}\]

Exceptions bei Thread Pool Verwendung

  • void execute(Runnable r)
    • In r.run() auftretende unbehandlete Exceptions werden sofort geworfen.
    • Der betroffene Pool-Thread terminiert.
  • Future<T> submit(Callable<T> c) \(\to\) f
    • In c.call() auftretende unbehandelte Exceptions werden “aufgehoben” und erst von f.get() geworfen.
    • Soll eine Ausnahme lokal in c.call() behandelt werden (z.B. zum Aufräumen), kann sie danach im catch-Block erneut geworfen werden (mit throw), um sie auch dem Aufrufer von f.get() weiterzuleiten.

Exkurs: CompletionService für voneinander unabhängige Tasks1

var pool = Executors.newCachedThreadPool();
var tasks = new ArrayList<Callable<String>>();
tasks.add(() -> "calc c1");
tasks.add(() -> "calc c2");
tasks.add(() -> "calc c3");
var completionService = new ExecutorCompletionService<String>(pool);
for (var callableTask : tasks) {
    completionService.submit(callableTask);
}
try {
    for (var i = 0; i < tasks.size(); i++) {
        var future = completionService.take();
        System.out.printf("Result %2d: %s\n", i, future.get());
    }
} catch (InterruptedException | ExecutionException e) {
    // ...
}
pool.shutdown();

Referenzen

Butcher, Paul. 2014. Seven Concurrency Models in Seven Weeks. When Threads Unravel. Dallas: The Pragmatic Programmers.
Goetz, Brian, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes und Doug Lea. 2006. Java concurrency in practice. Upper Saddle River, NJ: Addison-Wesley.