Passer au contenu principal

Asynchronous Updates

Ouvrir dans ChatGPT
25.02 Expérimental
Java API

L'API Environment.runLater() fournit un mécanisme pour mettre à jour en toute sécurité l'interface utilisateur depuis des threads d'arrière-plan dans les applications webforJ. Cette fonctionnalité expérimentale permet des opérations asynchrones tout en maintenant la sécurité des threads pour les modifications de l'interface utilisateur.

Fonctionnalité expérimentale
Cette fonctionnalité est expérimentale et peut changer ou être supprimée dans une future version.
AI skill available

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

Comprendre le modèle de thread

webforJ impose un modèle de threading strict où toutes les opérations de l'interface utilisateur doivent se produire sur le thread Environment. Cette restriction existe parce que :

  1. Contraintes de l'API webforJ : L'API webforJ sous-jacente est liée au thread qui a créé la session
  2. Affinité des threads des composants : Les composants de l'interface utilisateur maintiennent un état qui n'est pas sûr pour le threading
  3. Dispatch d'événements : Tous les événements de l'interface utilisateur sont traités séquentiellement sur un seul thread

Ce modèle à thread unique prévient les conditions de course et maintient un état cohérent pour tous les composants de l'interface utilisateur, mais crée des défis lorsqu'il s'agit de s'intégrer à des tâches de calcul asynchrones et de longue durée.

API RunLater

L'API Environment.runLater() fournit deux méthodes pour planifier les mises à jour de l'interface utilisateur :

Environment.java
// Planifier une tâche sans valeur de retour
public static PendingResult<Void> runLater(Runnable task)

// Planifier une tâche qui retourne une valeur
public static <T> PendingResult<T> runLater(Supplier<T> supplier)

Les deux méthodes retournent un PendingResult qui suit l'achèvement de la tâche et fournit un accès au résultat ou à toute exception survenue.

Héritage du contexte des threads

L'héritage automatique du contexte est une fonctionnalité critique de Environment.runLater(). Lorsqu'un thread fonctionnant dans un Environment crée des threads enfants, ces enfants héritent automatiquement de la capacité d'utiliser runLater().

Comment l'héritage fonctionne

Tout thread créé à partir d'un thread Environment a automatiquement accès à cet Environment. Cet héritage se produit automatiquement, donc vous n'avez pas besoin de passer de contexte ou de configurer quoi que ce soit.

@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();

public DataView() {
// Ce thread a le contexte d'Environment

// Les threads enfants héritent automatiquement du contexte
executor.submit(() -> {
String data = fetchRemoteData();

// Peut utiliser runLater car le contexte a été hérité
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

Threads sans contexte

Les threads créés en dehors du contexte Environment ne peuvent pas utiliser runLater() et lanceront une IllegalStateException :

// Initialisateur statique - aucun contexte d'Environment
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Lance IllegalStateException
}).start();
}

// Threads de minuterie système - aucun contexte d'Environment
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Lance IllegalStateException
}
}, 1000);

// Threads de bibliothèques externes - aucun contexte d'Environment
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Lance IllegalStateException
});

Comportement d'exécution

Le comportement d'exécution de runLater() dépend du thread qui l'appelle :

Depuis le thread d'interface utilisateur

Lorsqu'il est appelé depuis le thread Environment lui-même, les tâches s'exécutent synchroniquement et immédiatement :

button.onClick(e -> {
System.out.println("Avant : " + Thread.currentThread().getName());

PendingResult<String> result = Environment.runLater(() -> {
System.out.println("Intérieur : " + Thread.currentThread().getName());
return "terminé";
});

System.out.println("Après : " + result.isDone()); // true
});

Avec ce comportement synchrone, les mises à jour de l'interface utilisateur provenant des gestionnaires d'événements sont appliquées immédiatement et ne subissent pas de surcharge de mise en queue inutile.

Depuis des threads d'arrière-plan

Lorsqu'il est appelé depuis un thread d'arrière-plan, les tâches sont enfilées pour une exécution asynchrone :

@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Cela s'exécute sur le thread ForkJoinPool
System.out.println("Arrière-plan : " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(() -> {
// Cela s'exécute sur le thread Environment
System.out.println("Mise à jour de l'UI : " + Thread.currentThread().getName());
statusLabel.setText("Traitement terminé");
});

// result.isDone() serait false ici
// La tâche est enfilée et sera exécutée de manière asynchrone
});
}

webforJ traite les tâches soumises depuis des threads d'arrière-plan dans un ordre FIFO strict, préservant la séquence des opérations même lorsqu'elles sont soumises en parallèle depuis plusieurs threads. Avec cette garantie d'ordre, les mises à jour de l'interface utilisateur sont appliquées exactement dans l'ordre où elles ont été soumises. Ainsi, si le thread A soumet la tâche 1, puis que le thread B soumet la tâche 2, la tâche 1 s'exécutera toujours avant la tâche 2 sur le thread de l'interface utilisateur. Le traitement des tâches dans l'ordre FIFO empêche les incohérences dans l'interface utilisateur.

Annulation de tâche

Le PendingResult retourné par Environment.runLater() prend en charge l'annulation, vous permettant d'empêcher l'exécution des tâches en attente. En annulant les tâches en attente, vous pouvez éviter les fuites de mémoire et empêcher les opérations de longue durée de mettre à jour l'interface utilisateur après qu'elles ne soient plus nécessaires.

Annulation de base

PendingResult<Void> result = Environment.runLater(() -> {
updateUI();
});

// Annuler si pas encore exécuté
if (!result.isDone()) {
result.cancel();
}

Gestion de plusieurs mises à jour

Lors de l'exécution d'opérations de longue durée avec des mises à jour fréquentes de l'interface utilisateur, suivez tous les résultats en attente :

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);
});

// Suivre pour une potentielle annulation
pendingUpdates.add(update);

Thread.sleep(100);
}
});
}

public void cancelTask() {
isCancelled = true;

// Annuler toutes les mises à jour de l'UI en attente
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Gestion du cycle de vie des composants

Lorsque les composants sont détruits (par exemple, lors de la navigation), annulez toutes les mises à jour en attente pour éviter les fuites de mémoire :

@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();

@Override
protected void onDestroy() {
super.onDestroy();

// Annulez toutes les mises à jour en attente pour éviter les fuites de mémoire
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Considérations de conception

  1. Exigence de contexte : Les threads doivent avoir hérité d'un contexte Environment. Les threads de bibliothèques externes, les temporisateurs système et les initialisateurs statiques ne peuvent pas utiliser cette API.

  2. Prévention des fuites de mémoire : Suivez toujours et annulez les objets PendingResult dans les méthodes du cycle de vie des composants. Les lambdas enfilées capturent des références aux composants de l'interface utilisateur, empêchant la collecte des ordures si elles ne sont pas annulées.

  3. Exécution FIFO : Toutes les tâches s'exécutent dans un ordre FIFO strict, sans tenir compte de l'importance. Il n'y a pas de système de priorité.

  4. Limitations de l'annulation : L'annulation empêche uniquement l'exécution des tâches en file d'attente. Les tâches déjà en cours d'exécution se termineront normalement.

Étude de cas complète : LongTaskView

Ce qui suit est une implémentation complète, prête pour la production, démontrant toutes les meilleures pratiques pour des mises à jour asynchrones de l'interface utilisateur :

LongTaskView.java
startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Démarrage de la tâche d'arrière-plan...");
progressBar.setValue(0);
resultField.setValue("");

// Réinitialisez le drapeau annulé et effacez les mises à jour en attente précédentes
isCancelled = false;
pendingUIUpdates.clear();

// Démarrer la tâche d'arrière-plan avec un exécuteur explicite
// Remarque : cancel(true) interrompra le thread, provoquant Thread.sleep() pour lancer
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simuler une tâche longue avec 100 étapes
for (int i = 0; i <= 100; i++) {
// Vérifier si annulé
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("Tâche annulée !");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("La tâche a été annulée", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 secondes au total
} catch (InterruptedException e) {
// Le thread a été interrompu - sortie immédiate
Thread.currentThread().interrupt(); // Restaurer l'état d'interruption
return;
}

// Effectuer un calcul (déterministe pour la démo)
// Produits des valeurs entre 0 et 1
result += Math.sin(i) * 0.5 + 0.5;

// Mettre à jour la progression depuis le thread d'arrière-plan
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Traitement... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Mise à jour finale avec le résultat (ce code n'est atteint que si la tâche s'est terminée sans
// annulation)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("Tâche terminée !");
resultField.setValue("Résultat : " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Tâche d'arrière-plan terminée !", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

Analyse de l'étude de cas

Cette mise en œuvre démontre plusieurs motifs critiques :

1. Gestion du pool de threads

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • Utilise un exécuteur à un seul thread pour éviter l'épuisement des ressources
  • Crée des threads de démon qui ne bloqueront pas l'arrêt de la JVM

2. Suivi des mises à jour en attente

private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();

Chaque appel à Environment.runLater() est suivi pour permettre :

  • L'annulation lorsque l'utilisateur clique sur annuler
  • La prévention des fuites de mémoire dans onDestroy()
  • Un nettoyage approprié pendant le cycle de vie du composant

3. Annulation coopérative

private volatile boolean isCancelled = false;

Le thread d'arrière-plan vérifie ce drapeau à chaque itération, permettant :

  • Une réponse immédiate à l'annulation
  • Une sortie propre de la boucle
  • La prévention de mises à jour UI supplémentaires

4. Gestion du cycle de vie

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Réutilise la logique d'annulation
currentTask = null;
executor.shutdown();
}

Critique pour prévenir les fuites de mémoire en :

  • Annulant toutes les mises à jour UI en attente
  • Interrompant les threads en cours d'exécution
  • Arrêtant l'exécuteur

5. Tests de réactivité de l'UI

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Clic #" + count + " - L'UI est réactive !", Theme.GRAY);
});

Démontre que le thread de l'UI reste réactif pendant les opérations d'arrière-plan.