Asynchronous Updates
Die Environment.runLater() API bietet einen Mechanismus zum sicheren Aktualisieren der Benutzeroberfläche von Hintergrund-Threads in webforJ-Anwendungen. Dieses experimentelle Feature ermöglicht asynchrone Operationen, während die Thread-Sicherheit für UI-Änderungen aufrechterhalten wird.
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 ausgeführt werden müssen. Diese Einschränkung besteht aus folgenden Gründen:
- Einschränkungen der webforJ API: Die zugrunde liegende webforJ API bindet an den Thread, der die Sitzung erstellt hat.
- Thread-Affinität von Komponenten: UI-Komponenten behalten einen Zustand, der nicht threadsicher ist.
- Ereignisverarbeitung: Alle UI-Ereignisse werden sequenziell in einem einzelnen Thread verarbeitet.
Dieses einheitliche Thread-Modell verhindert Wettlaufbedingungen und erhält einen konsistenten Zustand für alle UI-Komponenten, schafft jedoch Herausforderungen bei der Integration mit asynchronen, lang laufenden Rechenaufgaben.
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 bietet.
Vererbung des Thread-Kontexts
Die automatische Vererbung des Kontexts ist ein grundlegendes Merkmal von Environment.runLater(). Wenn ein Thread, der in einem Environment läuft, Kind-Threads erstellt, erben diese 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 Environment-Kontext
// 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 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 von externen 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, von welchem Thread es aufgerufen wird:
Vom UI-Thread
Wenn es vom Environment-Thread selbst aufgerufen wird, werden die 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-Updates von Ereignishandlern sofort angewendet und verursachen keine unnötigen Warteschlangenüberhead.
Von Hintergrund-Threads
Wenn es von einem Hintergrund-Thread aufgerufen wird, werden die Aufgaben für die asynchrone Ausführung in der Warteschlange platziert:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Dies wird im ForkJoinPool-Thread ausgeführt
System.out.println("Hintergrund: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// Dies wird im Environment-Thread ausgeführt
System.out.println("UI-Aktualisierung: " + Thread.currentThread().getName());
statusLabel.setText("Verarbeitung abgeschlossen");
});
// result.isDone() wäre hier false
// Die Aufgabe ist in der Warteschlange und wird asynchron ausgeführt
});
}
webforJ verarbeitet Aufgaben, die von Hintergrund-Threads eingereicht werden, in strikter FIFO-Reihenfolge, um die Reihenfolge der Operationen beizubehalten, selbst wenn sie gleichzeitig von mehreren Threads eingereicht werden. Mit dieser Reihenfolge-Garantie werden UI-Updates in der genauen Reihenfolge angewendet, in der sie eingereicht wurden. Wenn also Thread A Aufgabe 1 einreicht und Thread B dann 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, das Ausführen von Warteschlangenaufgaben zu verhindern. Durch die Stornierung ausstehender Aufgaben können Sie Speicherlecks vermeiden und verhindern, dass lang laufende Operationen 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 Updates
Bei der Ausführung lang laufender Operationen mit häufigen UI-Aktualisierungen sollten Sie alle ausstehenden Ergebnisse verfolgen:
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 eine mögliche Stornierung
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Stornieren Sie alle ausstehenden UI-Updates
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Verwaltung des Lebenszyklus von Komponenten
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();
// Stornieren Sie alle ausstehenden Updates, um Speicherlecks zu vermeiden
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Designüberlegungen
-
Kontextanforderung: Threads müssen einen
Environment-Kontext vererbt haben. Threads von externen Bibliotheken, Systemtimern und statischen Initialisierern können diese API nicht verwenden. -
Vermeidung von Speicherlecks: Nachverfolgen und stornieren Sie immer
PendingResult-Objekte in den Methoden des Lebenszyklus von Komponenten. Warteschlangen-Lambdas erfassen Verweise auf UI-Komponenten und verhindern die Garbage Collection, wenn sie nicht storniert werden. -
FIFO-Ausführung: Alle Aufgaben werden in strikter 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 Warteschlangenaufgaben. Aufgaben, die bereits ausgeführt werden, werden normal abgeschlossen.
Vollständige Fallstudie: LongTaskView
Das Folgende ist eine vollständige, produktionsbereite Implementierung, die alle Best Practices für asynchrone UI-Updates demonstriert:
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;
});
- Verwendet einen einzelnen Thread-Executor, um Ressourcenermüdung zu verhindern
- Erstellt Daemon-Threads, die das Herunterfahren der JVM nicht verhindern
2. Nachverfolgung ausstehender Updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Jeder Aufruf von Environment.runLater() wird nachverfolgt, um Folgendes zu ermöglichen:
- Stornierung, wenn der Benutzer auf Stornieren klickt
- Vermeidung von Speicherlecks in
onDestroy() - Ordnungsgemäße Bereinigung während des Lebenszyklus der Komponente
3. Kooperative Stornierung
private volatile boolean isCancelled = false;
Der Hintergrund-Thread überprüft dieses Flag in jeder Iteration, was Folgendes ermöglicht:
- Sofortige Reaktion auf die Stornierung
- Sauberes Verlassen der Schleife
- Verhinderung weiterer UI-Aktualisierungen
4. Verwaltung des Lebenszyklus
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Wiederverwendet die Stornierungslogik
currentTask = null;
executor.shutdown();
}
Kritisch zur Vermeidung von Speicherlecks durch:
- Stornierung aller ausstehenden UI-Updates
- Unterbrechung 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 der Hintergrundoperationen reaktionsfähig bleibt.