Asynchronous Updates
Die Environment.runLater() API bietet einen Mechanismus zum sicheren Aktualisieren der Benutzeroberfläche von Hintergrundthreads in webforJ-Anwendungen. Dieses experimentelle Feature ermöglicht asynchrone Vorgänge, 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 zwingt ein strenges Thread-Modell auf, bei dem alle UI-Vorgänge im Environment-Thread stattfinden müssen. Diese Einschränkung besteht, weil:
- Einschränkungen der webforJ API: Die zugrunde liegende webforJ API ist an den Thread gebunden, der die Sitzung erstellt hat.
- Thread-Affinität der Komponenten: UI-Komponenten halten einen Zustand, der nicht threadsicher ist.
- Ereignisverarbeitung: Alle UI-Ereignisse werden sequenziell auf einem einzelnen Thread verarbeitet.
Dieses einäugige Modell verhindert Rennbedingungen und hält einen konsistenten Zustand für alle UI-Komponenten aufrecht, stellt jedoch eine Herausforderung dar, wenn man mit asynchronen, langanhaltenden Berechnungsaufgaben integriert.
RunLater API
Die Environment.runLater() API bietet zwei Methoden zur Planung von UI-Aktualisierungen:
// Planen einer Aufgabe ohne Rückgabewert
public static PendingResult<Void> runLater(Runnable task)
// Planen einer 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 alle aufgetretenen Ausnahmen bietet.
Vererbung des Thread-Kontexts
Die automatische Kontextvererbung ist ein kritisches Merkmal von Environment.runLater(). Wenn ein Thread, der in einem Environment-Thread läuft, Kind-Threads erstellt, erben diese Kinder automatisch die Fähigkeit, runLater() zu verwenden.
Wie die Vererbung funktioniert
Jeder Thread, der aus einem Environment-Thread erstellt wird, hat automatisch Zugriff auf dieses Environment. Diese Vererbung geschieht 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 Environment-Kontext
// Kind-Threads erben den Kontext automatisch
executor.submit(() -> {
String data = fetchRemoteData();
// Kann runLater verwenden, weil 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 Environment-Kontext
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Wirft IllegalStateException
}).start();
}
// Systemtimer-Threads - kein Environment-Kontext
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Wirft IllegalStateException
}
}, 1000);
// Threads externer Bibliotheken - kein Environment-Kontext
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Wirft IllegalStateException
});
Ausführungsverhalten
Das Ausführungsverhalten von runLater() hängt davon ab, welcher Thread ihn aufruft:
Vom UI-Thread
Wenn es vom Environment-Thread selbst aufgerufen wird, werden Aufgaben synchron und sofort ausgeführt:
button.onClick(e -> {
System.out.println("Vorher: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Innerhalb: " + Thread.currentThread().getName());
return "abgeschlossen";
});
System.out.println("Nachher: " + result.isDone()); // true
});
Mit diesem synchronen Verhalten werden UI-Aktualisierungen von Ereignis-Handlern sofort angewendet und verursachen keine unnötigen Warteschlangen-Overheads.
Von Hintergrund-Threads
Wenn es von einem Hintergrund-Thread aufgerufen wird, werden Aufgaben zur asynchronen Ausführung in die Warteschlange gestellt:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Dies läuft im 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 false
// Die Aufgabe ist in die Warteschlange gestellt und wird asynchron ausgeführt
});
}
webforJ verarbeitet Aufgaben, die von Hintergrund-Threads eingereicht werden, in strikter FIFO-Reihenfolge, wobei die Reihenfolge der Vorgänge beibehalten wird, auch wenn sie gleichzeitig von mehreren Threads eingereicht werden. Mit dieser Garantierung der Reihenfolge werden UI-Aktualisierungen in genau der Reihenfolge angewendet, in der sie eingereicht wurden. Wenn also Thread A Aufgabe 1 einreicht und dann Thread B Aufgabe 2 einreicht, wird Aufgabe 1 immer vor Aufgabe 2 im UI-Thread ausgeführt. Die Verarbeitung von Aufgaben in FIFO-Reihenfolge verhindert Inkonsistenzen in der UI.
Aufgabenstornierung
Das PendingResult, das von Environment.runLater() zurückgegeben wird, unterstützt die Stornierung, sodass Sie verhindern können, dass Warteschlangenaufgaben ausgeführt werden. Durch das Stornieren ausstehender Aufgaben können Sie Speicherlecks vermeiden und verhindern, dass langlaufende Vorgänge die UI aktualisieren, nachdem sie nicht mehr benötigt werden.
Grundlegende Stornierung
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Stornieren, wenn noch nicht ausgeführt
if (!result.isDone()) {
result.cancel();
}
Verwaltung mehrerer Aktualisierungen
Bei lang laufenden Operationen mit häufigen UI-Aktualisierungen sollten alle ausstehenden Ergebnisse verfolgt werden:
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);
});
// Für eine mögliche Stornierung verfolgen
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Alle ausstehenden UI-Aktualisierungen 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), sollten alle ausstehenden Aktualisierungen storniert werden, 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 Aktualisierungen stornieren, um Speicherlecks zu vermeiden
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Designüberlegungen
-
Kontextanforderung: Threads müssen einen
Environment-Kontext geerbt haben. Threads externer Bibliotheken, Systemtimer und statische Initialisierer können diese API nicht verwenden. -
Vermeidung von Speicherlecks: Verfolgen Sie immer
PendingResult-Objekte in Komponenten-Lebenszyklusmethoden. Warteschlangen-Lambdas erfassen Referenzen auf UI-Komponenten, was die Garbage Collection verhindert, wenn sie nicht storniert werden. -
FIFO-Ausführung: Alle Aufgaben werden in strikter FIFO-Reihenfolge ausgeführt, unabhängig von ihrer Wichtigkeit. Es gibt kein Prioritätssystem.
-
Limitierungen der Stornierung: Die Stornierung verhindert nur die Ausführung von Warteschlangenaufgaben. Bereits ausgeführte Aufgaben werden normal abgeschlossen.
Vollständige Fallstudie: LongTaskView
Die folgende vollständige, produktionsbereite Implementierung demonstriert alle Best Practices für asynchrone UI-Aktualisierungen:
Analyse der Fallstudie
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;
});
- Verwendung eines Executor mit einem einzelnen Thread, um Ressourcenerschöpfung zu vermeiden
- Erstellung von Daemon-Threads, die das Herunterfahren der JVM nicht verhindern
2. Verfolgen ausstehender Aktualisierungen
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Jeder Aufruf von Environment.runLater() wird verfolgt, um:
- Stornierung zu ermöglichen, wenn der Benutzer auf Abbrechen klickt
- Speicherlecks in
onDestroy()zu verhindern - Eine ordnungsgemäße Bereinigung während des Komponentenlebenszyklus sicherzustellen
3. Kooperative Stornierung
private volatile boolean isCancelled = false;
Der Hintergrund-Thread überprüft dieses Flag bei jeder Iteration, was ermöglicht:
- Sofortige Reaktion auf die Stornierung
- Sauberes Verlassen der Schleife
- Verhindern weiterer UI-Aktualisierungen
4. Verwaltung des Lebenszyklus
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Verwendet die Logik zur Stornierung wieder
currentTask = null;
executor.shutdown();
}
Kritisch zur Vermeidung von Speicherlecks durch:
- Stornieren aller ausstehenden UI-Aktualisierungen
- Unterbrechen der laufenden 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.