Asynchronous Updates
De Environment.runLater()
API biedt een mechanisme voor veilig bijwerken van de gebruikersinterface vanuit achtergrondthreads in webforJ-toepassingen. Deze experimentele functie maakt asynchrone operaties mogelijk terwijl de threadveiligheid voor UI-wijzigingen behouden blijft.
Deze API is gemarkeerd als experimenteel sinds 25.02 en kan in toekomstige versies veranderen. De API-handtekening, het gedrag en de prestatiekenmerken zijn onderhevig aan wijziging.
Begrijpen van het threadmodel
webforJ handhaaft een strikt threadingmodel waarbij alle UI-operaties op de Environment
-thread moeten plaatsvinden. Deze beperking bestaat omdat:
- webforJ API-beperkingen: De onderliggende webforJ API is gebonden aan de thread die de sessie heeft aangemaakt.
- Componentthreadaffiniteit: UI-componenten behouden status die niet thread-veilig is.
- Event-dispatch: Alle UI-gebeurtenissen worden sequentieel op een enkele thread verwerkt.
Dit single-threaded model voorkomt racecondities en behoudt een consistente staat voor alle UI-componenten, maar creëert uitdagingen bij integratie met asynchrone, langdurige berekeningstaken.
RunLater
API
De Environment.runLater()
API biedt twee methoden voor het plannen 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.
Threadcontextovererving
Automatische contextovererving is een belangrijke functie van Environment.runLater()
. Wanneer een thread die in een Environment
draait kindthreads aanmaakt, erven die kinderen automatisch het vermogen om runLater()
te gebruiken.
Hoe overerving werkt
Elke thread die vanuit een Environment
-thread wordt aangemaakt, heeft automatisch toegang tot die Environment
. Deze overerving 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 de 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
genereren:
// Statische initialisator - 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:
Vanaf de UI-thread
Wanneer het vanuit de Environment
-thread zelf wordt aangeroepen, worden taken synchronisch en onmiddellijk uitgevoerd:
button.onClick(e -> {
System.out.println("Voorafgaand: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Binnen: " + Thread.currentThread().getName());
return "voltooid";
});
System.out.println("Erna: " + result.isDone()); // true
});
Met dit synchrone gedrag worden UI-updates vanuit gebeurtenishandlers onmiddellijk toegepast en komen geen onnodige wachtrijkosten met zich mee.
Vanuit achtergrondthreads
Wanneer het vanuit een achtergrondthread wordt aangeroepen, worden taken gequeued 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 voltooid");
});
// result.isDone() zou hier false zijn
// De taak is gequeue en zal asynchroon worden uitgevoerd
});
}
webforJ verwerkt taken die van achtergrondthreads zijn ingediend in strikte FIFO-volgorde, waardoor de volgorde van operaties behouden blijft, zelfs wanneer ze gelijktijdig vanaf meerdere threads zijn ingediend. Met deze ordeningsgarantie worden UI-updates in de exacte volgorde toegepast waarin ze zijn ingediend. Dus als thread A taak 1 indient, en daarna 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 door Environment.runLater()
wordt geretourneerd, ondersteunt annulering, waardoor je kunt voorkomen dat gequeue 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 het nog niet is uitgevoerd
if (!result.isDone()) {
result.cancel();
}
Beheren van meerdere updates
Bij het uitvoeren van langdurige bewerkingen met frequente UI-updates, volg alle lopende 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 lopende UI-updates
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Componentlevenscyclusbeheer
Wanneer componenten worden vernietigd (bijvoorbeeld 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, systeemtijden en statische initializers kunnen deze API niet gebruiken. -
Geheugenlekpreventie: Houd altijd
PendingResult
-objecten bij en annuleer ze in de componentlevenscyclusmethoden. Gequeue lambdas vangen referenties 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.
-
Annuleringsbeperkingen: Annulering voorkomt alleen de uitvoering van gequeue taken. Taken die al worden uitgevoerd, worden normaal afgerond.
Volledige casestudy: LongTaskView
Het volgende is een complete, productieklare implementatie die alle beste praktijken voor asynchrone UI-updates demonstreert:
Casestudy-analyse
Deze implementatie demonstreert verschillende kritieke patronen:
1. Threadpoolbeheer
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Gebruikt een enkele threadexecutor om resource-uitputting te voorkomen.
- Creëert daemonthreads die de JVM-afsluiting niet zullen blokkeren.
2. Volgen van lopende updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Elke aanroep van Environment.runLater()
wordt bijgehouden om:
- Annulering mogelijk te maken wanneer de gebruiker op annuleren klikt.
- Geheugenlekpreventie in
onDestroy()
. - Juiste opruiming tijdens de componentlevenscyclus mogelijk te maken.
3. Coöperatieve annulering
private volatile boolean isCancelled = false;
De achtergrondthread controleert deze vlag bij elke iteratie, waardoor:
- Onmiddellijke reactie op annulering mogelijk is.
- Schoon vertrek uit de lus mogelijk is.
- Voorkoming van verdere UI-updates mogelijk is.
4. Levenscyclusbeheer
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Hergebruikt annuleringslogica
currentTask = null;
executor.shutdown();
}
Kritisch voor het voorkomen van geheugenlekken door:
- Alle lopende UI-updates te annuleren.
- Lopende threads te onderbreken.
- De executor af te sluiten.
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.