Asynchronous Updates
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.
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:
- Einschränkungen der webforJ API: Die zugrunde liegende webforJ API ist an den Thread gebunden, der die Sitzung erstellt hat.
- Thread-Zugehörigkeit von Komponenten: UI-Komponenten halten Zustände, die nicht threadsicher sind.
- 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:
// 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
-
Kontextanforderung: Threads müssen einen
Environment-Kontext geerbt haben. Threads aus externen Bibliotheken, Systemtimern und statischen Initialisierern können diese API nicht verwenden. -
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. -
FIFO-Ausführung: Alle Aufgaben werden in strenger FIFO-Reihenfolge ausgeführt, unabhängig von der Wichtigkeit. Es gibt kein Prioritätssystem.
-
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:
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.