Saltar al contenido principal

Asynchronous Updates

Abrir en ChatGPT
25.02 Experimental
Java API

La API Environment.runLater() proporciona un mecanismo para actualizar de manera segura la interfaz de usuario desde hilos en segundo plano en aplicaciones webforJ. Esta característica experimental permite operaciones asincrónicas mientras se mantiene la seguridad de los hilos para las modificaciones de la interfaz de usuario.

Característica experimental
Esta función es experimental y puede cambiar o eliminarse en una versión futura.
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."

Comprendiendo el modelo de hilos

webforJ impone un modelo de hilo estricto donde todas las operaciones de la interfaz de usuario deben ocurrir en el hilo Environment. Esta restricción existe porque:

  1. Restricciones de la API de webforJ: La API subyacente de webforJ está vinculada al hilo que creó la sesión.
  2. Afinidad del hilo del componente: Los componentes de la interfaz de usuario mantienen un estado que no es seguro para múltiples hilos.
  3. Distribución de eventos: Todos los eventos de la interfaz de usuario son procesados secuencialmente en un solo hilo.

Este modelo de un solo hilo previene condiciones de carrera y mantiene un estado consistente para todos los componentes de la interfaz de usuario, pero crea desafíos al integrarse con tareas de cálculo asincrónicas y de larga duración.

API RunLater

La API Environment.runLater() proporciona dos métodos para programar actualizaciones de la interfaz de usuario:

Environment.java
// Programar una tarea sin valor de retorno
public static PendingResult<Void> runLater(Runnable task)

// Programar una tarea que devuelve un valor
public static <T> PendingResult<T> runLater(Supplier<T> supplier)

Ambos métodos devuelven un PendingResult que rastrea la finalización de la tarea y proporciona acceso al resultado o cualquier excepción que haya ocurrido.

Herencia del contexto del hilo

La herencia automática del contexto es una característica crítica de Environment.runLater(). Cuando un hilo que se ejecuta en un Environment crea hilos hijos, esos hijos heredan automáticamente la capacidad de usar runLater().

Cómo funciona la herencia

Cualquier hilo creado desde dentro de un hilo Environment tiene automáticamente acceso a ese Environment. Esta herencia ocurre de manera automática, por lo que no necesitas pasar ningún contexto ni configurar nada.

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

public DataView() {
// Este hilo tiene contexto de Environment

// Los hilos hijos heredan el contexto automáticamente
executor.submit(() -> {
String data = fetchRemoteData();

// Puede usar runLater porque el contexto fue heredado
Environment.runLater(() -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}

Hilos sin contexto

Los hilos creados fuera del contexto de Environment no pueden usar runLater() y lanzarán una IllegalStateException:

// Inicializador estático - sin contexto de Environment
static {
new Thread(() -> {
Environment.runLater(() -> {}); // Lanza IllegalStateException
}).start();
}

// Hilos de temporizador del sistema - sin contexto de Environment
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
Environment.runLater(() -> {}); // Lanza IllegalStateException
}
}, 1000);

// Hilos de bibliotecas externas - sin contexto de Environment
httpClient.sendAsync(request, responseHandler)
.thenAccept(response -> {
Environment.runLater(() -> {}); // Lanza IllegalStateException
});

Comportamiento de ejecución

El comportamiento de ejecución de runLater() depende de qué hilo lo llama:

Desde el hilo de la interfaz de usuario

Cuando se llama desde el hilo Environment mismo, las tareas se ejecutan síncronamente e inmediatamente:

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

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

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

Con este comportamiento síncrono, las actualizaciones de la interfaz de usuario desde los controladores de eventos se aplican de inmediato y no incurren en ningún costo innecesario de cola.

Desde hilos en segundo plano

Cuando se llama desde un hilo en segundo plano, las tareas son enviadas para ejecución asincrónica:

@Override
public void onDidCreate() {
CompletableFuture.runAsync(() -> {
// Esto se ejecuta en un hilo de ForkJoinPool
System.out.println("En segundo plano: " + Thread.currentThread().getName());

PendingResult<Void> result = Environment.runLater(() -> {
// Esto se ejecuta en el hilo de Environment
System.out.println("Actualización de la UI: " + Thread.currentThread().getName());
statusLabel.setText("Procesamiento completo");
});

// result.isDone() sería falso aquí
// La tarea está en cola y se ejecutará de manera asincrónica
});
}

webforJ procesa tareas enviadas desde hilos en segundo plano en estricto orden FIFO, preservando la secuencia de operaciones incluso cuando se envían desde múltiples hilos de manera concurrente. Con esta garantía de orden, las actualizaciones de la interfaz se aplican en el mismo orden en que fueron enviadas. Entonces, si el hilo A envía la tarea 1 y luego el hilo B envía la tarea 2, la tarea 1 siempre se ejecutará antes que la tarea 2 en el hilo de la interfaz de usuario. Procesar tareas en orden FIFO previene inconsistencias en la interfaz de usuario.

Cancelación de tareas

El PendingResult devuelto por Environment.runLater() admite la cancelación, lo que te permite evitar que las tareas en cola se ejecuten. Al cancelar tareas pendientes, puedes evitar fugas de memoria y evitar que operaciones de larga duración actualicen la interfaz de usuario después de que ya no son necesarias.

Cancelación básica

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

// Cancelar si aún no se ha ejecutado
if (!result.isDone()) {
result.cancel();
}

Administración de múltiples actualizaciones

Al realizar operaciones de larga duración con actualizaciones frecuentes de la interfaz de usuario, rastrea todos los resultados pendientes:

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

// Rastrear para posible cancelación
pendingUpdates.add(update);

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

public void cancelTask() {
isCancelled = true;

// Cancelar todas las actualizaciones pendientes de la UI
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Gestión del ciclo de vida de componentes

Cuando los componentes son destruidos (por ejemplo, durante la navegación), cancela todas las actualizaciones pendientes para evitar fugas de memoria:

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

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

// Cancelar todas las actualizaciones pendientes para evitar fugas de memoria
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}

Consideraciones de diseño

  1. Requisito de contexto: Los hilos deben haber heredado un contexto de Environment. Los hilos de bibliotecas externas, temporizadores del sistema y inicializadores estáticos no pueden usar esta API.

  2. Prevención de fugas de memoria: Siempre rastrea y cancela objetos PendingResult en métodos de ciclo de vida de componentes. Las lambdas en cola capturan referencias a componentes de la interfaz de usuario, previniendo la recolección de basura si no se cancela.

  3. Ejecución FIFO: Todas las tareas se ejecutan en estricto orden FIFO independientemente de su importancia. No hay un sistema de prioridades.

  4. Limitaciones de cancelación: La cancelación solo evita la ejecución de tareas en cola. Las tareas ya en ejecución se completarán normalmente.

Estudio de caso completo: LongTaskView

Lo siguiente es una implementación completa y lista para producción que demuestra todas las mejores prácticas para actualizaciones asincrónicas de la interfaz de usuario:

LongTaskView.java
startButton.setEnabled(false);
cancelButton.setEnabled(true);
statusField.setValue("Iniciando tarea en segundo plano...");
progressBar.setValue(0);
resultField.setValue("");

// Reiniciar bandera de cancelación y limpiar actualizaciones pendientes anteriores
isCancelled = false;
pendingUIUpdates.clear();

// Iniciar tarea en segundo plano con ejecutor explícito
// Nota: cancel(true) interrumpirá el hilo, causando que Thread.sleep() lance
// InterruptedException
currentTask = CompletableFuture.runAsync(() -> {
double result = 0;

// Simular una tarea larga con 100 pasos
for (int i = 0; i <= 100; i++) {
// Verificar si fue cancelada
if (isCancelled) {
PendingResult<Void> cancelUpdate = Environment.runLater(() -> {
statusField.setValue("¡Tarea cancelada!");
progressBar.setValue(0);
resultField.setValue("");
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("La tarea fue cancelada", Theme.GRAY);
});
pendingUIUpdates.add(cancelUpdate);
return;
}

try {
Thread.sleep(100); // 10 segundos en total
} catch (InterruptedException e) {
// El hilo fue interrumpido - salir inmediatamente
Thread.currentThread().interrupt(); // Restaurar estado interrumpido
return;
}

// Hacer algún cálculo (determinista para demostración)
// Produce valores entre 0 y 1
result += Math.sin(i) * 0.5 + 0.5;

// Actualizar el progreso desde el hilo en segundo plano
final int progress = i;
PendingResult<Void> updateResult = Environment.runLater(() -> {
progressBar.setValue(progress);
statusField.setValue("Procesando... " + progress + "%");
});
pendingUIUpdates.add(updateResult);
}

// Actualización final con el resultado (este código solo se alcanza si la tarea se completó sin
// cancelación)
if (!isCancelled) {
final double finalResult = result;
PendingResult<Void> finalUpdate = Environment.runLater(() -> {
statusField.setValue("¡Tarea completada!");
resultField.setValue("Resultado: " + String.format("%.2f", finalResult));
startButton.setEnabled(true);
cancelButton.setEnabled(false);
showToast("¡Tarea en segundo plano finalizada!", Theme.SUCCESS);
});
pendingUIUpdates.add(finalUpdate);
}
}, executor);
}

Análisis del estudio de caso

Esta implementación demuestra varios patrones críticos:

1. Gestión del grupo de hilos

private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
  • Utiliza un ejecutor de un solo hilo para prevenir la exhaustión de recursos.
  • Crea hilos daemon que no evitarán el apagado de la JVM.

2. Rastreo de actualizaciones pendientes

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

Cada llamada a Environment.runLater() es rastreada para permitir:

  • Cancelación cuando el usuario hace clic en cancelar.
  • Prevención de fugas de memoria en onDestroy().
  • Limpieza adecuada durante el ciclo de vida del componente.

3. Cancelación cooperativa

private volatile boolean isCancelled = false;

El hilo en segundo plano verifica esta bandera en cada iteración, permitiendo:

  • Respuesta inmediata a la cancelación.
  • Salida limpia del bucle.
  • Prevención de actualizaciones adicionales de la interfaz de usuario.

4. Gestión del ciclo de vida

@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Reutiliza la lógica de cancelación
currentTask = null;
executor.shutdown();
}

Crítico para prevenir fugas de memoria mediante:

  • Cancelación de todas las actualizaciones pendientes de la UI.
  • Interrupción de hilos en ejecución.
  • Apagado del ejecutor.

5. Pruebas de receptividad de la UI

testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Clic #" + count + " - ¡LA UI es receptiva!", Theme.GRAY);
});

Demuestra que el hilo de la interfaz de usuario permanece receptivo durante operaciones en segundo plano.