Ga naar hoofdinhoud

Asynchronous Updates

Open in ChatGPT
25.02 Experimental
Java API

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.

Experimentele API

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:

  1. webforJ API-beperkingen: De onderliggende webforJ API is gebonden aan de thread die de sessie heeft aangemaakt.
  2. Componentthreadaffiniteit: UI-componenten behouden status die niet thread-veilig is.
  3. 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:

Environment.java
// 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

  1. Contextvereiste: Threads moeten een Environment-context hebben geërfd. Externe bibliotheekthreads, systeemtijden en statische initializers kunnen deze API niet gebruiken.

  2. 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.

  3. FIFO-uitvoering: Alle taken worden in strikte FIFO-volgorde uitgevoerd, ongeacht het belang. Er is geen prioriteitssysteem.

  4. 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:

LongTaskView.java
    startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Achtergrondtaak starten...");
progressBar.setValue(0);
resultField.setValue("");

// Reset geannuleerde vlag en wis vorige lopende updates
isCancelled = false;
pendingUIUpdates.clear();

// Start achtergrondtaak met expliciete executor
// Opmerking: cancel(true) zal de thread onderbreken, wat leidt tot Thread.sleep() dat een
// InterruptedException gooit
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simuleer lange taak met 100 stappen
for (int i = 0; i <= 100; i++) {
// Controleer of geannuleerd
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("Taak geannuleerd!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Taak is geannuleerd", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // Totaal 10 seconden
} catch (InterruptedException e) {
// Thread was onderbroken - verlaat onmiddellijk
Thread.currentThread().interrupt(); // Herstel onderbroken status
return;
}

// Voer een berekening uit (deterministisch voor demo)
// Produceert waarden tussen 0 en 1
result += Math.sin(i) * 0.5 + 0.5;

// Werk voortgang bij vanuit achtergrondthread
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Verwerken... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Laatste update met resultaat (deze code wordt alleen bereikt als de taak is voltooid zonder
// annulering)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("Taak voltooid!");
resultField.setValue("Resultaat: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Achtergrondtaak voltooid!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

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.