Asynchronous Updates
De Environment.runLater() API biedt een mechanisme voor veilig updaten van de UI vanuit achtergrondthreads in webforJ-applicaties. Deze experimentele functie maakt asynchrone bewerkingen mogelijk terwijl de threadveiligheid voor UI-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 handhaaft een strict threadingmodel waarbij alle UI-bewerkingen moeten plaatsvinden op de Environment-thread. Deze beperking bestaat om de volgende redenen:
- Beperkingen van de webforJ API: De onderliggende webforJ API is gebonden aan de thread die de sessie heeft aangemaakt.
- Threadaffiniteit van componenten: UI-componenten behouden een status die niet threadveilig is.
- Evenementdispatching: Alle UI-evenementen worden sequentieel op een enkele thread verwerkt.
Dit single-threaded model voorkomt racecondities en handhaaft een consistente status voor alle UI-componenten, maar creëert uitdagingen bij integratie met asynchrone, langdurige computertaken.
RunLater API
De Environment.runLater() API biedt twee methoden voor het inplannen van UI-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 het resultaat of eventuele opgetreden uitzonderingen.
Erfelijkheid van threadcontext
Automatische contextovererving is een kritieke functie van Environment.runLater(). Wanneer een thread die draait in een Environment-thread kindthreads aanmaakt, erven die kinderen automatisch de mogelijkheid om runLater() te gebruiken.
Hoe erfelijkheid werkt
Elke thread die wordt aangemaakt vanuit een Environment-thread heeft automatisch toegang tot die Environment. Deze erfelijkheid gebeurt automatisch, zodat je geen context hoeft door te geven of iets hoeft te configureren.
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// Deze thread heeft Environment-context
// Kindthreads 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 UI-thread
Wanneer aangeroepen vanuit de Environment-thread zelf, worden taken synchronisch en onmiddellijk uitgevoerd:
button.onClick(e -> {
System.out.println("Vooraf: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Binnen: " + Thread.currentThread().getName());
return "voltooid";
});
System.out.println("Daarna: " + result.isDone()); // true
});
Met dit synchrone gedrag worden UI-updates van gebeurtenishandlers onmiddellijk toegepast en wordt onnodige wachttijd voorkomen.
Van achtergrondthreads
Wanneer aangeroepen vanuit een achtergrondthread, worden taken in de wacht gezet 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 Environment-thread
System.out.println("UI-update: " + Thread.currentThread().getName());
statusLabel.setText("Verwerking voltooid");
});
// result.isDone() zou hier false zijn
// De taak is in de wacht gezet en zal asynchroon worden uitgevoerd
});
}
webforJ verwerkt taken die zijn ingediend vanuit achtergrondthreads in strikte FIFO-volgorde, waardoor de volgorde van bewerkingen behouden blijft, zelfs wanneer ze gelijktijdig vanuit meerdere threads worden ingediend. Met deze volgorde garantie worden UI-updates toegepast in de exacte volgorde waarin ze zijn ingediend. Dus als thread A taak 1 indient en thread B taak 2 indient, zal taak 1 altijd vóór taak 2 op de UI-thread worden uitgevoerd. Het verwerken van taken in FIFO-volgorde voorkomt inconsistenties in de UI.
Taakannulering
De PendingResult die wordt geretourneerd door Environment.runLater() ondersteunt annulering, zodat je kunt voorkomen dat in de wacht gezette taken worden uitgevoerd. Door lopende taken te annuleren, kun je geheugenlekken vermijden en voorkomen dat langdurige bewerkingen de UI 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 langdurige bewerkingen met frequente UI-updates, houd alle lopende resultaten bij:
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);
});
// Houd bij voor mogelijke annulering
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Annuleer alle lopende UI-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 dan alle lopende 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 lopende 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. Externe bibliotheekthreads, systeemtimers en statische initialisatoren kunnen deze API niet gebruiken. -
Voorkomen van geheugenlekken: Houd altijd
PendingResult-objecten bij en annuleer ze in de methoden van de levenscyclus van componenten. In de wacht geplaatste lambdas vangen verwijzingen naar UI-componenten, waardoor garbage collection wordt voorkomen als ze niet worden geannuleerd. -
FIFO-uitvoering: Alle taken worden in strikte FIFO-volgorde uitgevoerd, ongeacht het belang. Er is geen prioriteitssysteem.
-
Beperkingen van annulering: Annulering voorkomt alleen de uitvoering van in de wacht gezette taken. Taken die al worden uitgevoerd, worden normaal voltooid.
Compleet casestudy: LongTaskView
Het onderstaande is een complete, productieklare implementatie die alle best practices voor asynchrone UI-updates demonstreert:
Analyse van de casestudy
Deze implementatie demonstreert verschillende kritieke patronen:
1. Beheer van threadpool
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Gebruikt een enkele thread-executor om uitputting van middelen te voorkomen
- Creëert daemondraden die de JVM-shutdown niet tegenhouden
2. Bijhouden van lopende updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Elke aanroep van Environment.runLater() wordt bijgehouden om te kunnen:
- Annuleren wanneer de gebruiker op annuleren klikt
- Voorkomen van geheugenlekken in
onDestroy() - Juiste opruiming tijdens de levenscyclus van componenten
3. Coöperatieve annulering
private volatile boolean isCancelled = false;
De achtergrondthread controleert deze vlag bij elke iteratie, wat mogelijk maakt:
- Directe reactie op annulering
- Schone exit uit de lus
- Voorkomen van verdere UI-updates
4. Beheer van de levenscyclus
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Hergebruikt annuleringlogica
currentTask = null;
executor.shutdown();
}
Kritiek voor het voorkomen van geheugenlekken door:
- Annuleren van alle in de wacht gezette UI-updates
- Onderbreken van actieve threads
- Stoppen van de executor
5. Testen van UI-responsiviteit
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klik #" + count + " - UI is responsief!", Theme.GRAY);
});
Demonstreert dat de UI-thread responsief blijft tijdens achtergrondbewerkingen.