Asynchronous Updates
De Environment.runLater() API biedt een mechanisme voor het veilig bijwerken van de gebruikersinterface vanuit achtergrondthreads in webforJ-toepassingen. Deze experimentele functie maakt asynchrone bewerkingen mogelijk terwijl de thread-veiligheid voor UI-wijzigingen behouden blijft.
Begrijpen van het threadmodel
webforJ handhaaft een strikt threadmodel waarbij alle UI-bewerkingen moeten plaatsvinden op de Environment thread. Deze beperking bestaat omdat:
- webforJ API-beperkingen: De onderliggende webforJ API bindt aan de thread die de sessie heeft aangemaakt
- Thread affiniteit van componenten: UI-componenten behouden staat die niet thread-veilig is
- Evenementverwerking: Alle UI-events worden sequentieel op één 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 computertaken.
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 uitzonderingen die zich hebben voorgedaan.
Thread contextoverdracht
Automatische contextoverdracht is een cruciale functie van Environment.runLater(). Wanneer een thread die in een Environment draait kindthreads aanmaakt, erven die kindthreads automatisch de mogelijkheid om runLater() te gebruiken.
Hoe overdracht werkt
Elke thread die vanuit een Environment thread wordt gemaakt, heeft automatisch toegang tot die Environment. Deze overdracht gebeurt automatisch, zodat u 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 geerfd
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Threads zonder context
Threads die buiten de Environment context zijn aangemaakt kunnen runLater() niet gebruiken en zullen een IllegalStateException genereren:
// Statische initialisator - geen Environment-context
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Throw IllegalStateException
}).start();
}
// Systeemtimerthreads - geen Environment-context
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Throw IllegalStateException
}
}, 1000);
// Externe bibliotheekthreads - geen Environment-context
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Throw IllegalStateException
});
Uitvoeringsgedrag
Het uitvoeringsgedrag van runLater() hangt af van welke thread het aanroept:
Van de UI-thread
Wanneer het wordt 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 synchronische gedrag worden UI-updates vanuit gebeurtenishandlers onmiddellijk toegepast en brengt het geen onnodige wachtrijoverhead met zich mee.
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 Environment thread
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Verwerking voltooid");
});
// result.isDone() zou hier false zijn
// De taak is gepland en zal asynchroon worden uitgevoerd
});
}
webforJ verwerkt taken die vanuit achtergrondthreads zijn ingediend in strikte FIFO-volgorde, waarbij de volgorde van bewerkingen wordt behouden, zelfs wanneer deze gelijktijdig vanuit meerdere threads worden ingediend. Met deze volgordegarantie worden UI-updates in de exacte volgorde toegepast waarin ze zijn ingediend. Als thread A taak 1 indient, en vervolgens thread B taak 2 indient, dan zal taak 1 altijd vóór taak 2 worden uitgevoerd op de UI-thread. Het verwerken van taken in FIFO-volgorde voorkomt inconsistenties in de UI.
Taakannulering
De PendingResult die door Environment.runLater() wordt geretourneerd ondersteunt annulering, zodat u kunt voorkomen dat geplande taken worden uitgevoerd. Door lopende taken te annuleren, kunt u geheugenlekken vermijden en voorkomen dat langdurige bewerkingen de UI bijwerken nadat ze niet meer nodig zijn.
Basisannulering
PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});
// Annuleer als nog niet uitgevoerd
if (!result.isDone()) {
result.cancel();
}
Beheersen van meerdere updates
Bij het uitvoeren van langdurige operaties 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 ter voorkoming van 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 (bijvoorbeeld tijdens navigatie), annuleer 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
Environmentcontext hebben geërfd. Externe bibliotheekthreads, systeemtimers en statische initialisatoren kunnen deze API niet gebruiken. -
Voorkoming van geheugenlekken: Houd altijd
PendingResultobjecten bij en annuleer deze in methoden voor de levenscyclus van componenten. Geplande lambdas vangen referenties naar UI-componenten, waardoor garbage collection wordt voorkomen als ze niet worden geannuleerd. -
FIFO-uitvoering: Alle taken worden uitgevoerd in strikte FIFO-volgorde, ongeacht het belang. Er is geen prioriteitssysteem.
-
Annuleringsbeperkingen: Annulering voorkomt alleen de uitvoering van geplande taken. Taken die al worden uitgevoerd, zullen normaal worden voltooid.
Volledige case study: LongTaskView
De volgende is een complete, productieklare implementatie die alle beste praktijken voor asynchrone UI-updates demonstreert:
Case study-analyse
Deze implementatie demonstreert verschillende kritische 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 thread-executor om uitputting van middelen te voorkomen
- Creeërt daemonthreads die de JVM-afsluiting niet kunnen voorkomen
2. Volgen van lopende updates
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Elke Environment.runLater() aanroep wordt bijgehouden om:
- Annulering mogelijk te maken wanneer de gebruiker op annuleren klikt
- Voorkomen van geheugenlekken in
onDestroy() - Juiste opruiming tijdens de levenscyclus van de component
3. Coöperatieve annulering
private volatile boolean isCancelled = false;
De achtergrondthread controleert deze vlag bij elke iteratie, waardoor mogelijk is:
- Een onmiddellijke reactie op annulering
- Een schone uitgang uit de lus
- Voorkomen van verdere UI-updates
4. Levenscyclusbeheer
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Hergebruikt annulering logica
currentTask = null;
executor.shutdown();
}
Cruciaal voor het voorkomen van geheugenlekken door:
- Het annuleren van alle lopende UI-updates
- Het onderbreken van actieve threads
- Het veilig afsluiten van de executor
5. Testen van UI-responsiviteit
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klik #" + count + " - UI is responsief!", Theme.GRAY);
});
Toont aan dat de UI-thread responsief blijft tijdens achtergrondbewerkingen.