Asynchronous Updates
Die Environment.runLater()
API bietet einen Mechanismus zum sicheren Aktualisieren der Benutzeroberfläche aus Hintergrundthreads in webforJ-Anwendungen. Diese experimentelle Funktion ermöglicht asynchrone Operationen, während die Threadsicherheit für UI-Modifikationen gewährleistet bleibt.
Diese API wird seit 25.02 als experimentell eingestuft und kann sich in zukünftigen Versionen ändern. Die API-Signatur, das Verhalten und die Leistungsmerkmale können modifiziert werden.
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:
- webforJ API-Beschränkungen: Die zugrunde liegende webforJ-API ist an den Thread gebunden, der die Sitzung erstellt hat.
- Thread-Affinität der Komponenten: UI-Komponenten behalten einen Zustand, der nicht thread-sicher ist.
- Ereignisverarbeitung: Alle UI-Ereignisse werden sequenziell in einem einzigen Thread verarbeitet.
Dieses eindrähtige Modell verhindert Wettlaufbedingungen und sorgt für einen konsistenten Zustand aller UI-Komponenten, schafft jedoch Herausforderungen bei der Integration von asynchronen, langlaufenden Berechnungsaufgaben.
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 auf aufgetretene Ausnahmen bietet.
Vererbung des Thread-Kontexts
Die automatische Kontextvererbung ist ein wichtiges Merkmal von Environment.runLater()
. Wenn ein Thread, der im Environment
ausgeführt wird, untergeordnete 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 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 Kontext des Environment
// Kindthreads 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
:
// Statische Initialisierung - kein Environment-Kontext
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Wirft IllegalStateException
}).start();
}
// Systemzeituhr-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, welcher Thread es aufruft:
Vom UI-Thread
Wenn aus dem Environment
-Thread selbst aufgerufen, 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 Ereignishandlern sofort angewendet und verursachen keine unnötigen Warteschlangenüberlastungen.
Aus Hintergrund-Threads
Wenn aus einem Hintergrundthread aufgerufen, werden Aufgaben für asynchrone Ausführung in die Warteschlange gestellt:
@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 wird 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 Operationen beibehalten wird, auch wenn sie gleichzeitig von mehreren Threads eingereicht werden. Mit dieser Reihenfolgegarantie 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 eine Stornierung, die es Ihnen ermöglicht, zu verhindern, dass wartende Aufgaben ausgeführt werden. Durch das Stornieren ausstehender Aufgaben können Sie Speicherlecks vermeiden und verhindern, dass langlaufende Operationen die Benutzeroberfläche 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 Durchführung langlaufender 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);
});
// Verfolgen für 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 Komponentenlebenszyklus
Wenn Komponenten zerstört werden (z. B. während der Navigation), stornieren Sie alle ausstehenden Aktualisierungen, 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 Aktualisierungen, um Speicherlecks zu vermeiden
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Designüberlegungen
-
Anforderung des Kontexts: Threads müssen einen
Environment
-Kontext geerbt haben. Threads von externen Bibliotheken, Systemtimern und statischen Initialisierern können diese API nicht verwenden. -
Vermeidung von Speicherlecks: Verfolgen und stornieren Sie immer
PendingResult
-Objekte in den Methoden des Komponentenlebenszyklus. Wartende Lambdas erfassen Verweise auf UI-Komponenten, was die Garbage Collection verhindert, wenn sie nicht storniert werden. -
FIFO-Ausführung: Alle Aufgaben werden unabhängig von der Wichtigkeit in strikter FIFO-Reihenfolge ausgeführt. Es gibt kein Prioritätssystem.
-
Einschränkungen der Stornierung: Die Stornierung verhindert nur die Ausführung wartender Aufgaben. Bereits ausführende Aufgaben werden normal abgeschlossen.
Vollständige Fallstudie: LongTaskView
Die folgende Implementierung ist vollständig und produktionsbereit und demonstriert alle Best Practices für asynchrone UI-Aktualisierungen:
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 einzelnen Thread-Executor, um Ressourcenauslastung zu vermeiden
- Erstellt Daemon-Threads, die das Herunterfahren der JVM nicht verhindern
2. Verfolgung ausstehender Aktualisierungen
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Jeder Environment.runLater()
-Aufruf wird verfolgt, um zu ermöglichen:
- Stornierung, wenn der Benutzer auf Stornieren klickt
- Vermeidung von Speicherlecks in
onDestroy()
- Ordnungsgemäße Bereinigung während des Komponentenlebenszyklus
3. Kooperative Stornierung
private volatile boolean isCancelled = false;
Der Hintergrundthread überprüft dieses Flag in jeder Iteration, was ermöglicht:
- Sofortige Reaktion auf Stornierung
- Sauberes Verlassen der Schleife
- Verhinderung weiterer UI-Aktualisierungen
4. Verwaltung des Lebenszyklus
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Wiederverwendung der Stornierungslogik
currentTask = null;
executor.shutdown();
}
Kritisch zur Vermeidung von Speicherlecks durch:
- Stornieren aller ausstehenden UI-Aktualisierungen
- Unterbrechen von laufenden Threads
- Herunterfahren des Executors
5. Test der Reaktionsfähigkeit der UI
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.