Asynchronous Updates
De Environment.runLater() API biedt een mechanisme voor veilig bijwerken van de gebruikersinterface vanuit achtergrondthreads in webforJ-applicaties. Deze experimentele functie maakt asynchrone operaties mogelijk terwijl de threadveiligheid voor de gebruikersinterface-wijzigingen behouden blijft.
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."
Begrijpen van het threadmodel
webforJ handhaaf een strikt threadingmodel waarbij alle gebruikersinterface-operaties moeten plaatsvinden op de Environment-thread. Deze restrictie bestaat omdat:
- webforJ API-beperkingen: De onderliggende webforJ API is gebonden aan de thread die de sessie heeft aangemaakt
- Component thread-affiniteit: Gebruikersinterfacecomponenten behouden een staat die niet thread-veilig is
- Evenementdispatching: Alle gebruikersinterface-evenementen worden sequieel op een enkele thread verwerkt
Dit single-threaded model voorkomt racecondities en behoudt een consistente staat voor alle gebruikersinterfacecomponenten, maar creëert uitdagingen bij de integratie met asynchrone, langlopende computationele taken.
RunLater API
De Environment.runLater() API biedt twee methoden voor het plannen van gebruikersinterface-updates:
// Plan een taak zonder retourwaarde
public static PendingResult<Void> runLater(Runnable task)
// Plan een taak die een waarde retourneert
public static <T> PendingResult<T> runLater(Supplier<T> supplier)
Beide methoden retourneren een PendingResult die de voltooiing van de taak bijhoudt en toegang biedt tot de resultaten of eventuele uitzonderingen die zijn opgetreden.
Threadcontextovereenkomst
Automatische contextovereenkomst is een cruciale functie van Environment.runLater(). Wanneer een thread die draait in een Environment childthreads aanmaakt, erven die kinderen automatisch de mogelijkheid om runLater() te gebruiken.
Hoe overeenstemming werkt
Elke thread die wordt gemaakt vanuit een Environment-thread heeft automatisch toegang tot die Environment. Deze overeenkomst gebeurt automatisch, dus je hoeft geen context door te geven of iets te configureren.
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// Deze thread heeft Environment-context
// Childthreads erven de context automatisch
executor.submit(() -> {
String data = fetchRemoteData();
// Kan runLater gebruiken omdat de context is geërfd
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Threads zonder context
Threads die buiten de Environment-context zijn gemaakt, kunnen runLater() niet gebruiken en zullen een IllegalStateException veroorzaken:
// Statische initializer - geen Environment-context
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Gooi IllegalStateException
}).start();
}
// Systeemtimerthreads - geen Environment-context
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Gooi IllegalStateException
}
}, 1000);
// Externe bibliotheekthreads - geen Environment-context
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Gooi IllegalStateException
});
Uitvoeringsgedrag
Het uitvoeringsgedrag van runLater() hangt af van welke thread het aanroept:
Van de gebruikersinterface-thread
Wanneer aangeroepen vanuit de Environment-thread zelf, worden taken sychronisch en onmiddellijk uitgevoerd:
button.onClick(e -> {
System.out.println("Voor: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Binnen: " + Thread.currentThread().getName());
return "voltooid";
});
System.out.println("Na: " + result.isDone()); // true
});
Met dit synchrone gedrag worden gebruikersinterface-updates vanuit gebeurtenishandlers onmiddellijk toegepast en wordt er geen onnodige wachtende overhead gegenereerd.
Van achtergrondthreads
Wanneer aangeroepen vanuit een achtergrondthread, worden taken gepland voor asynchrone uitvoering:
@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Dit draait op ForkJoinPool-thread
System.out.println("Achtergrond: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(() -> {
// Dit draait op de Environment-thread
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Verwerking compleet");
});
// result.isDone() zou hier false zijn
// De taak is gepland en zal asynchroon worden uitgevoerd
});
}
webforJ verwerkt taken die zijn ingediend vanuit achtergrondthreads in strikte FIFO-volgorde, waardoor de volgorde van operaties wordt behouden, zelfs wanneer ze gelijktijdig vanuit meerdere threads zijn ingediend. Met deze ordergarantie worden gebruikersinterface-updates toegepast in de exacte volgorde waarin ze zijn ingediend. Dus als thread A taak 1 indient, en vervolgens thread B taak 2 indient, zal taak 1 altijd vóór taak 2 worden uitgevoerd op de gebruikersinterface-thread. Het verwerken van taken in FIFO-volgorde voorkomt inconsistenties in de gebruikersinterface.
Taakannulering
De PendingResult die door Environment.runLater() wordt geretourneerd ondersteunt annulering, waardoor je kunt voorkomen dat geplande taken worden uitgevoerd. Door uitstaande taken te annuleren, kun je geheugenlekken vermijden en voorkomen dat langlopende operaties de gebruikersinterface bijwerken nadat ze niet langer nodig zijn.
Basisannulering
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Annuleer als nog niet uitgevoerd
if (!result.isDone()) {
result.cancel();
}
Beheren van meerdere updates
Bij het uitvoeren van langlopende operaties met frequente gebruikersinterface-updates, volg je alle uitstaande resultaten:
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);
});
// Volg voor mogelijke annulering
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Annuleer alle uitstaande gebruikersinterface-updates
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Beheer van de levenscyclus van componenten
Wanneer componenten worden vernietigd (bijv. tijdens navigatie), annuleer je alle uitstaande updates om geheugenlekken te voorkomen:
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// Annuleer alle uitstaande updates om geheugenlekken te voorkomen
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Ontwerpoverwegingen
-
Contextvereiste: Threads moeten een
Environment-context hebben geërfd. Draad bibliotheekthreads, systeem timers, en statische initializers kunnen deze API niet gebruiken. -
Voorkomen van geheugenlekken: Volg altijd
PendingResult-objecten en annuleer deze in componentlevenscyclusmethoden. Geplande lambdas leggen referenties vast naar gebruikersinterfacecomponenten, waardoor garbage collection wordt voorkomen als ze niet geannuleerd worden. -
FIFO-uitvoering: Alle taken worden in strikte FIFO-volgorde uitgevoerd, ongeacht het belang. Er is geen prioriteitssysteem.
-
Beperkingen van annuleringen: Annulering voorkomt alleen de uitvoering van geplande taken. Taken die al worden uitgevoerd, worden normaal beëindigd.
Volledige casestudy: LongTaskView
Het volgende is een complete, productieklare implementatie die alle best practices voor asynchrone gebruikersinterface-updates demonstreert:
Analyse van de casestudy
Deze implementatie demonstreert verschillende kritieke patronen:
1. Beheer van thread pools
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Gebruik een enkele thread-executor om uitputting van middelen te voorkomen
- Maak daemon threads die de JVM-afsluiting niet zullen voorkomen
2. Volgen van uitstaande updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Elke aanroep van Environment.runLater() wordt gevolgd om:
- Annulering mogelijk te maken wanneer de gebruiker op annuleren klikt
- Voorkomen van geheugenlekken in
onDestroy() - Zorg voor een goede opruiming tijdens de levenscyclus van componenten
3. Coöperatieve annulering
private volatile boolean isCancelled = false;
De achtergrondthread controleert deze vlag bij elke iteratie, waardoor mogelijk wordt:
- Onmiddellijke reactie op annulering
- Schone exit uit de lus
- Voorkomen van verdere gebruikersinterface-updates
4. Beheer van de levenscyclus
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Hergebruikt annuleringslogica
currentTask = null;
executor.shutdown();
}
Kritisch voor het voorkomen van geheugenlekken door:
- Annuleren van alle uitstaande gebruikersinterface-updates
- Onderbreken van draaiende threads
- Het afsluiten van de executor
5. Testen van de responsiviteit van de gebruikersinterface
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klik #" + count + " - UI is responsief!", Theme.GRAY);
});
Demonstreert dat de gebruikersinterface-thread responsief blijft tijdens achtergrondoperaties.