Zum Hauptinhalt springen

Asynchronous Updates

In ChatGPT öffnen
25.02 Experimentell
Java API

Die Environment.runLater() API bietet einen Mechanismus zum sicheren Aktualisieren der Benutzeroberfläche von Hintergrundthreads in webforJ-Anwendungen. Diese experimentelle Funktion ermöglicht asynchrone Operationen, während die Thread-Sicherheit für UI-Änderungen gewährleistet bleibt.

Experimentelles Feature
Dieses Feature ist experimentell und kann sich in einer zukünftigen Version ändern oder entfernt werden.
AI skill available

The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:

  • "Refresh this dashboard every 30 seconds."
  • "Add a search-as-you-type debouncer."
  • "Run this CPU-heavy work in the background and update the progress bar."

Verständnis des Thread-Modells

webforJ erzwingt ein strenges Thread-Modell, bei dem alle UI-Operationen im Environment-Thread stattfinden müssen. Diese Einschränkung existiert, weil:

  1. Einschränkungen der webforJ API: Die zugrunde liegende webforJ API ist an den Thread gebunden, der die Sitzung erstellt hat.
  2. Thread-Zugehörigkeit von Komponenten: UI-Komponenten halten Zustände, die nicht threadsicher sind.
  3. Ereignisverarbeitung: Alle UI-Ereignisse werden sequenziell in einem einzigen Thread verarbeitet.

Dieses ein-Threadmodell verhindert Race-Conditions und sorgt für einen konsistenten Zustand aller UI-Komponenten, schafft jedoch Herausforderungen, wenn es um die Integration mit asynchronen, langlaufenden Berechnungsaufgaben geht.

RunLater API

Die Environment.runLater() API bietet zwei Methoden zur Planung von UI-Aktualisierungen:

Environment.java
// Planen Sie eine Aufgabe ohne Rückgabewert
public static PendingResult<Void> runLater(Runnable task)

// Planen Sie eine Aufgabe, die einen Wert zurückgibt
public static <T> PendingResult<T> runLater(Supplier<T> supplier)

Beide Methoden geben ein PendingResult zurück, das den Abschluss der Aufgabe verfolgt und Zugriff auf das Ergebnis oder aufgetretene Ausnahmen gewährt.

Erbschaft des Thread-Kontexts

Die automatische Kontextvererbung ist eine kritische Funktion von Environment.runLater(). Wenn ein Thread, der in einem Environment läuft, Kind-Threads erstellt, erben diese Kinder automatisch die Fähigkeit, runLater() zu verwenden.

Wie die Vererbung funktioniert

Jeder Thread, der innerhalb eines Environment-Threads erstellt wird, hat automatisch Zugriff auf dieses Environment. Diese Vererbung erfolgt automatisch, sodass Sie keinen Kontext übergeben oder etwas konfigurieren müssen.

@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();

public DataView() {
// Dieser Thread hat den Kontext von Environment

// Kind-Threads erben den Kontext automatisch
executor.submit(() -> {
String data = fetchRemoteData();

// Kann runLater verwenden, da der Kontext vererbt wurde
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

Threads ohne Kontext

Threads, die außerhalb des Environment-Kontexts erstellt werden, können runLater() nicht verwenden und werfen eine IllegalStateException:

// Statischer Initialisierer - kein Kontext von Environment
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Wirft IllegalStateException
}).start();
}

// System-Timer-Threads - kein Kontext von Environment
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Wirft IllegalStateException
}
}, 1000);

// Threads aus externen Bibliotheken - kein Kontext von Environment
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Wirft IllegalStateException
});

Ausführungsverhalten

Das Ausführungsverhalten von runLater() hängt davon ab, welcher Thread es aufruft:

Vom UI-Thread

Wenn es vom Environment-Thread selbst aufgerufen wird, führen Aufgaben synchron und sofort aus:

button.onClick(e -> {
System.out.println("Vorher: " + Thread.currentThread().getName());

PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Innere: " + Thread.currentThread().getName());
return "vollständig";
});

System.out.println("Nachher: " + result.isDone()); // true
});

Mit diesem synchronen Verhalten werden UI-Updates von Ereignis-Handlern sofort angewendet und es entstehen keine unnötigen Warteschlangenüberhänge.

Von Hintergrundthreads

Wenn es von einem Hintergrundthread aufgerufen wird, werden Aufgaben für die asynchrone Ausführung in die Warteschlange gestellt:

@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Dies läuft auf einem ForkJoinPool-Thread
System.out.println("Hintergrund: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(() -> {
// Dies läuft im Environment-Thread
System.out.println("UI-Aktualisierung: " + Thread.currentThread().getName());
statusLabel.setText("Verarbeitung abgeschlossen");
});

// result.isDone() wäre hier falsch
// Die Aufgabe wird in die Warteschlange gestellt und wird asynchron ausgeführt
});
}

webforJ verarbeitet Aufgaben, die von Hintergrundthreads eingereicht werden, in strenger FIFO-Reihenfolge und behält die Reihenfolge der Operationen auch bei gleichzeitiger Einreichung von mehreren Threads bei. Mit dieser Ordnungsgewährleistung werden UI-Updates in der genauen Reihenfolge angewendet, in der sie eingereicht wurden. Wenn also Thread A Aufgabe 1 einreicht und Thread B Aufgabe 2 einreicht, wird Aufgabe 1 immer vor Aufgabe 2 im UI-Thread ausgeführt. Das Verarbeiten von Aufgaben in FIFO-Reihenfolge verhindert Inkonsistenzen in der UI.

Aufgabenstornierung

Das PendingResult, das von Environment.runLater() zurückgegeben wird, unterstützt die Stornierung und ermöglicht es Ihnen, die Ausführung von geplanten Aufgaben zu verhindern. Durch das Abbrechen ausstehender Aufgaben können Sie Speicherlecks vermeiden und verhindern, dass lang laufende Operationen die UI aktualisieren, wenn sie nicht mehr benötigt werden.

Grundlegende Stornierung

PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});

// Abbrechen, wenn noch nicht ausgeführt
if (!result.isDone()) {
result.cancel();
}

Verwaltung mehrerer Updates

Bei lang laufenden Vorgängen mit häufigen UI-Updates verfolgen Sie alle ausstehenden Ergebnisse:

public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;

public void startTask() {
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;

final int progress = i;
PendingResult<Void> update = Environment.runLater(() -> {
progressBar.setValue(progress);
});

// Nachverfolgung für mögliche Stornierung
pendingUpdates.add(update);

Thread.sleep(100);
}
});
}

public void cancelTask() {
isCancelled = true;

// Alle ausstehenden UI-Updates stornieren
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Verwaltung des Komponentenlebenszyklus

Wenn Komponenten zerstört werden (z.B. während der Navigation), stornieren Sie alle ausstehenden Updates, um Speicherlecks zu vermeiden:

@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();

@Override
protected void onDestroy() {
super.onDestroy();

// Alle ausstehenden Updates stornieren, um Speicherlecks zu vermeiden
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Entwurfserwägungen

  1. Kontextanforderung: Threads müssen einen Environment-Kontext geerbt haben. Threads aus externen Bibliotheken, Systemtimern und statischen Initialisierern können diese API nicht verwenden.

  2. Verhinderung von Speicherlecks: Verfolgen und stornieren Sie immer PendingResult-Objekte in Komponentenlebenszyklusmethoden. In Warteschlangen stehende Lambdas erfassen Referenzen auf UI-Komponenten, was die Garbage Collection verhindert, wenn sie nicht storniert werden.

  3. FIFO-Ausführung: Alle Aufgaben werden in strenger FIFO-Reihenfolge ausgeführt, unabhängig von der Wichtigkeit. Es gibt kein Prioritätssystem.

  4. Einschränkungen bei der Stornierung: Die Stornierung verhindert nur die Ausführung von wartenden Aufgaben. Bereits ausgeführte Aufgaben werden normal abgeschlossen.

Vollständige Fallstudie: LongTaskView

Die folgende Implementierung ist eine komplette, produktionsbereite Umsetzung, die alle Best Practices für asynchrone UI-Aktualisierungen demonstriert:

LongTaskView.java
  startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Hintergrundaufgabe gestartet...");
progressBar.setValue(0);
resultField.setValue("");

// Setzen Sie das Abbruchkennzeichen zurück und löschen Sie vorherige ausstehende Updates
isCancelled = false;
pendingUIUpdates.clear();

// Starten Sie die Hintergrundaufgabe mit explizitem Executor
// Hinweis: cancel(true) unterbricht den Thread, wodurch Thread.sleep() eine
// InterruptedException wirft
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simulieren Sie eine lang laufende Aufgabe mit 100 Schritten
for (int i = 0; i <= 100; i++) {
// Überprüfen Sie, ob abgebrochen wurde
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("Aufgabe abgebrochen!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Aufgabe wurde abgebrochen", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // insgesamt 10 Sekunden
} catch (InterruptedException e) {
// Der Thread wurde unterbrochen - sofort aussteigen
Thread.currentThread().interrupt(); // Wiederherstellung des Unterbrechungsstatus
return;
}

// Führen Sie einige Berechnungen durch (deterministisch für die Demo)
// Erzeugt Werte zwischen 0 und 1
result += Math.sin(i) * 0.5 + 0.5;

// Update den Fortschritt aus dem Hintergrundthread
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Verarbeitung... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Letztes Update mit Ergebnis (dieser Code wird nur erreicht, wenn die Aufgabe ohne
// Stornierung abgeschlossen wurde)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("Aufgabe abgeschlossen!");
resultField.setValue("Ergebnis: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Hintergrundaufgabe abgeschlossen!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

Fallstudienanalyse

Diese Implementierung demonstriert mehrere kritische Muster:

1. Verwaltung des Thread-Pools

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • Verwendet einen Einzel-Thread-Executor, um Ressourcenerschöpfung zu verhindern
  • Erstellt Daemon-Threads, die das Herunterfahren der JVM nicht verhindern

2. Verfolgen ausstehender Updates

private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();

Jeder Environment.runLater()-Aufruf wird nachverfolgt, um Folgendes zu ermöglichen:

  • Stornierung, wenn der Benutzer auf Abbrechen klickt
  • Verhinderung von Speicherlecks in onDestroy()
  • ordnungsgemäße Bereinigung während des Komponentenlebenszyklus

3. Kooperative Stornierung

private volatile boolean isCancelled = false;

Der Hintergrundthread überprüft dieses Kennzeichen bei jeder Iteration, was Folgendes ermöglicht:

  • Sofortige Reaktion auf die Stornierung
  • Sauberer Ausstieg aus der Schleife
  • Vermeidung weiterer UI-Updates

4. Verwaltung des Lebenszyklus

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Wiederverwendet die Stornierungslogik
currentTask = null;
executor.shutdown();
}

Kritisch zur Vermeidung von Speicherlecks durch:

  • Abbrechen aller ausstehenden UI-Updates
  • Unterbrechen laufender Threads
  • Herunterfahren des Executors

5. Testen der UI-Reaktionsfähigkeit

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klick #" + count + " - UI ist reaktionsfähig!", Theme.GRAY);
});

Demonstriert, dass der UI-Thread während Hintergrundoperationen reaktionsfähig bleibt.