Passer au contenu principal

Mises à jour asynchrones

Ouvrir dans ChatGPT
25.02 Preview
Java API

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

Preview feature
This feature is in preview. Its API may still change in future releases.
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 threads​

webforJ impose un modèle de threads strict selon lequel toutes les opérations sur l’interface utilisateur doivent avoir lieu sur le thread Environment. Cette restriction s’explique par les raisons suivantes :

  1. Contraintes de l’API webforJ : l’API webforJ sous-jacente est liée au thread qui a créé la session.
  2. Affinité des composants avec les threads : les composants de l’interface utilisateur conservent un état qui n’est pas sécurisé pour une utilisation concurrente.
  3. Distribution des événements : tous les événements de l’interface utilisateur sont traités séquentiellement sur un seul thread.

Ce modèle à thread unique évite les conditions de concurrence et assure un état cohérent pour tous les composants de l’interface utilisateur, mais pose des difficultés lors de l’intégration avec des tâches de calcul asynchrones et de longue durée.

Chaque session utilisateur possède son propre Environment et son propre thread Environment. Un thread d’arrière-plan n’appartient à aucune session : il faut donc lui indiquer quel Environment mettre à jour.

API RunLater​

L’API Environment.runLater() propose deux méthodes pour planifier des mises à jour de l’interface utilisateur. Toutes deux prennent l’Environment cible comme premier argument :

Environment.java
// Schedule a task with no return value
public static PendingResult<Void> runLater(Environment target, Runnable task)

// Schedule a task that returns a value
public static <T> PendingResult<T> runLater(Environment target, Supplier<T> supplier)

Les deux méthodes renvoient un PendingResult qui suit l’achèvement de la tâche et permet d’accéder au résultat ou aux exceptions qui se sont produites.

Variantes sans cible

Les variantes runLater(Runnable) et runLater(Supplier) déduisent la cible à partir du thread qui a créé le thread appelant. Dans un pool de threads partagé par plusieurs utilisateurs, chaque tâche s’exécute dans l’Environment de l’utilisateur dont l’appel a créé ce thread du pool. Ces variantes sont obsolètes depuis la version 26.10 et leur suppression est prévue pour la prochaine version majeure. Passez toujours la cible en argument.

Passer la cible​

Environment.getCurrent() renvoie l’Environment du thread actuel. Sur le thread Environment — celui sur lequel s’exécutent les constructeurs, les gestionnaires d’événements et les méthodes de cycle de vie — cette méthode renvoie l’Environment de la session. Sur un thread d’arrière-plan, elle renvoie null.

Récupérez la cible là où elle est disponible, avant le démarrage du travail en arrière-plan, puis transmettez-la à ce travail :

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

public DataView() {
// The constructor runs on the Environment thread
Environment env = Environment.getCurrent();

executor.submit(() -> {
String data = fetchRemoteData();

Environment.runLater(env, () -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

Puisque la cible est passée en argument, l’appel fonctionne depuis n’importe quel thread : un thread créé par la vue, un thread Spring @Async, le ForkJoinPool commun, un thread de minuterie ou un thread de rappel d’un client HTTP.

Récupérer la cible depuis le mauvais thread​

La seule erreur à éviter consiste à appeler Environment.getCurrent() à l’intérieur du travail en arrière-plan. Cette méthode y renvoie null, et runLater() déclenche une NullPointerException :

executor.submit(() -> {
String data = fetchRemoteData();

// Wrong: getCurrent() returns null on a background thread
Environment.runLater(Environment.getCurrent(), () -> dataLabel.setText(data));
});

La même règle s’applique aux initialiseurs statiques, aux threads java.util.Timer et aux rappels de bibliothèques externes. Aucun d’eux ne possède d’Environment : la cible doit donc provenir d’un champ, d’un paramètre ou d’une variable locale capturée, renseignée sur le thread Environment.

Comportement à l’exécution​

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

Depuis le thread de l’interface utilisateur​

Lorsqu’elle est appelée depuis le thread Environment de la cible, les tâches s’exécutent de façon synchrone et immédiate :

button.onClick(e -> {
Environment env = Environment.getCurrent();
System.out.println("Before: " + Thread.currentThread().getName());

PendingResult<String> result = Environment.runLater(env, () -> {
System.out.println("Inside: " + Thread.currentThread().getName());
return "completed";
});

System.out.println("After: " + result.isDone()); // true
});

Grâce à ce comportement synchrone, les mises à jour de l’interface utilisateur effectuées depuis les gestionnaires d’événements sont appliquées immédiatement, sans surcoût inutile dû à leur mise en file d’attente.

Depuis des threads d’arrière-plan​

Lorsqu’elles sont appelées depuis un autre thread, les tâches sont mises en file d’attente pour une exécution asynchrone :

@Override
public void onDidCreate() {
Environment env = Environment.getCurrent();

CompletableFuture.runAsync(() -> {
// This runs on a ForkJoinPool thread
System.out.println("Background: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(env, () -> {
// This runs on the Environment thread
System.out.println("UI Update: " + Thread.currentThread().getName());
statusLabel.setText("Processing complete");
});

// result.isDone() would be false here
// The task is queued and will execute asynchronously
});
}

webforJ traite les tâches soumises depuis des threads d’arrière-plan dans un ordre FIFO strict, préservant ainsi la séquence des opérations même lorsque plusieurs threads les soumettent simultanément. Cette garantie d’ordre assure que 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 le thread B soumet la tâche 2, la tâche 1 s’exécute toujours avant la tâche 2 sur le thread de l’interface utilisateur. Le traitement des tâches dans l’ordre FIFO évite les incohérences dans l’interface utilisateur.

Annulation des tâches​

Le PendingResult renvoyé par Environment.runLater() prend en charge l’annulation, ce qui permet d’empêcher l’exécution des tâches mises en file d’attente. L’annulation des tâches en attente permet d’éviter les fuites de mémoire et d’empêcher les opérations de longue durée de mettre à jour l’interface utilisateur lorsqu’elles ne sont plus nécessaires.

Annulation de base​

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

// Cancel if not yet executed
if (!result.isDone()) {
result.cancel();
}

Gestion de plusieurs mises à jour​

Lors de l’exécution d’opérations de longue durée qui effectuent fréquemment des mises à jour 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() {
Environment env = Environment.getCurrent();

CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;

final int progress = i;
PendingResult<Void> update = Environment.runLater(env, () -> {
progressBar.setValue(progress);
});

// Track for potential cancellation
pendingUpdates.add(update);

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

public void cancelTask() {
isCancelled = true;

// Cancel all pending UI updates
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Gestion du cycle de vie des composants​

Lorsque des 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();

// Cancel all pending updates to prevent memory leaks
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Considérations de conception​

  1. Cible requise : l’Environment cible est récupéré avec Environment.getCurrent() sur le thread Environment, puis transmis au travail en arrière-plan. Le récupérer sur un thread d’arrière-plan renvoie null.

  2. Prévention des fuites de mémoire : suivez et annulez toujours les objets PendingResult dans les méthodes du cycle de vie des composants. Les expressions lambda mises en file d’attente capturent des références aux composants de l’interface utilisateur, ce qui empêche leur récupération par le ramasse-miettes si elles ne sont pas annulées.

  3. Exécution FIFO : toutes les tâches s’exécutent dans un ordre FIFO strict, quelle que soit leur importance. Il n’existe aucun système de priorité.

  4. Limites de l’annulation : l’annulation empêche uniquement l’exécution des tâches mises en file d’attente. Les tâches déjà en cours d’exécution se terminent normalement.

Étude de cas complète : LongTaskView​

Voici une implémentation complète, prête pour la production, qui illustre les bonnes pratiques pour les mises à jour asynchrones de l’interface utilisateur :

LongTaskView.java
startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Starting background task...");
progressBar.setValue(0);
resultField.setValue("");

// Reset cancelled flag and clear previous pending updates
isCancelled = false;
pendingUIUpdates.clear();

// Read the target Environment on the UI thread, before the background task starts
Environment env = Environment.getCurrent();

// Start background task with explicit executor
// Note: cancel(true) will interrupt the thread, causing Thread.sleep() to throw
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simulate long task with 100 steps
for (int i = 0; i <= 100; i++) {
// Check if cancelled
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(env, () -> {
statusField.setValue("Task cancelled!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Task was cancelled", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 seconds total
} catch (InterruptedException e) {
// Thread was interrupted - exit immediately
Thread.currentThread().interrupt(); // Restore interrupted status
return;
}

// Do some calculation (deterministic for demo)
// Produces values between 0 and 1
result += Math.sin(i) * 0.5 + 0.5;

// Update progress from background thread
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(env, () -> {
progressBar.setValue(progress);
statusField.setValue("Processing... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Final update with result (this code is only reached if the task completed without
// cancellation)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(env, () -> {
statusField.setValue("Task completed!");
resultField.setValue("Result: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("Background task finished!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

Analyse de l’étude de cas​

Cette implémentation illustre plusieurs modèles essentiels :

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 à thread unique pour éviter l’épuisement des ressources.
  • Crée des threads démons qui n’empêcheront pas l’arrêt de la JVM.

2. Définition de la cible​

Environment env = Environment.getCurrent();

startLongTask() s’exécute sur le thread Environment : il y récupère donc la cible, puis le travail en arrière-plan la transmet à chaque appel de Environment.runLater(). Le même code fonctionne tel quel avec un pool de threads partagé à l’échelle de l’application.

3. Suivi des mises à jour en attente​

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

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

  • l’annulation lorsque l’utilisateur clique sur le bouton d’annulation ;
  • la prévention des fuites de mémoire dans onDestroy() ;
  • un nettoyage approprié au cours du cycle de vie du composant.

4. Annulation coopérative​

private volatile boolean isCancelled = false;

Le thread d’arrière-plan vérifie cet indicateur à chaque itération, ce qui permet :

  • une réponse immédiate à l’annulation ;
  • une sortie propre de la boucle ;
  • la prévention de mises à jour supplémentaires de l’interface utilisateur.

5. Gestion du cycle de vie​

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reuses cancellation logic
currentTask = null;
executor.shutdown();
}

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

  • annulant toutes les mises à jour de l’interface utilisateur en attente ;
  • interrompant les threads en cours d’exécution ;
  • arrêtant l’exécuteur.

6. Test de la réactivité de l’interface utilisateur​

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Click #" + count + " - UI is responsive!", Theme.GRAY);
});

Montre que le thread de l’interface utilisateur reste réactif pendant les opérations en arrière-plan.

Continuez à apprendre.