Net-Base Revista

24.07.2026

Delphi: TParallel.For con interfaz de progreso segura para hilos (TThread.Queue) sin interbloqueos

Cómo combinar Delphi TParallel.For con una interfaz de progreso segura para hilos: actualizaciones mediante TThread.Queue, agregación limpia, gestión de cancelación y las trampas típicas de deadlock en depuración y en operación.

24.07.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

Quien en Delphi quiera paralelizar trabajos intensivos en CPU o con alta E/S, llega rápidamente a la Parallel Programming Library (PPL) y, en concreto, a TParallel.For. El efecto suele ser medible de inmediato, hasta que en el mismo instante se quiere actualizar una UI de progreso “rápidamente”. Ahí surgen los bloqueos típicos: congelamientos aparentemente aleatorios de la interfaz, una ProgressBar que salta hacia atrás, o un deadlock completo cuando se avanza paso a paso en el depurador.

En este artículo se trata de TParallel.For UI de progreso segura para hilos: un patrón robusto que funciona en VCL y FMX, que agrupa de forma ordenada las actualizaciones de la UI mediante TThread.Queue, tiene en cuenta Cancel/Abort y evita de forma consistente las trampas de deadlock más comunes. El foco está en la realidad operativa: comportamiento reproducible, responsabilidades claras y pistas de depuración que también ayudan cuando el fallo solo ocurre “en cliente”.

Por qué la UI de progreso con TParallel.For falla tan a menudo

TParallel.For se ejecuta típicamente en hilos de trabajo del threadpool de Delphi. Esos hilos no deben tocar controles de VCL o FMX directamente, porque los frameworks de UI (Message Loop, Window Handles, Rendering) están ligados al hilo principal. Incluso una aparentemente inofensiva ProgressBar.Position := … desde un worker puede provocar un comportamiento indefinido: AVs esporádicas, ventanas congeladas o actualizaciones “parpadeantes”.

La reparación obvia suele ser TThread.Synchronize. Esto resuelve la seguridad de hilos, pero en bucles paralelos conduce rápidamente a otro problema: se crea un cuello de botella serial. Cada worker espera al hilo de la UI, que a su vez está ocupado renderizando y procesando llamadas a Synchronize. Bajo carga eso parece un deadlock —aunque sea “solo” un efecto de starvation/lockstep.

Y luego está la clase de deadlock real: el hilo principal espera (por ejemplo mediante WaitFor, Task.Wait o indirectamente por llamadas bloqueantes) al fin de la operación paralela, mientras los worker-threads intentan enviar trabajo al hilo principal vía Synchronize o por un uso desafortunado de Queue. Resultado: el hilo principal espera a los workers, los workers esperan al hilo principal.

TThread.Queue vs. TThread.Synchronize: la diferencia práctica

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Durante la depuración se distingue rápidamente si los hilos de trabajo esperan al hilo principal o solo encolan actualizaciones.

Ambos mecanismos sirven para ejecutar código de forma segura en el hilo principal. La diferencia está en la semántica de espera:

  • TThread.Synchronize: El worker que llama espera hasta que el hilo principal ha ejecutado el código. Esto es “síncrono”, aumenta la latencia y es un ingrediente clásico para deadlocks cuando el hilo principal está bloqueado.
  • TThread.Queue: El hilo de trabajo coloca el código únicamente en una cola para el hilo principal y continúa ejecutando. Esto es «asíncrono», desacopla los hilos y en escenarios paralelos suele ser la mejor opción por defecto —siempre que se controle la frecuencia de actualizaciones.

Importante: Queue no es un pase libre. Si en cada iteración de un bucle envía una actualización a la cola, inundará la cola del hilo principal. La UI no se queda bloqueada por un deadlock, pero sí por la mera cantidad de mensajes. La UI se percibe «lenta», y el fin del procesamiento se retrasa porque quedan pendientes cientos o miles de actualizaciones de UI.

El caso límite que realmente duele: esperar en el hilo de la UI

En aplicaciones empresariales es frecuente este flujo: se pulsa el botón «Start», la UI se desactiva, se abre un ProgressDialog, y a continuación se espera de forma síncrona hasta que todo termine para luego reactivar. Este patrón es el núcleo de muchos deadlocks.

Variantes típicas (según la base de código):

  • El hilo principal inicia TParallel.For y luego invoca una lógica de espera bloqueante (directa o indirectamente).
  • Un ProgressDialog llama en el Constructor o en OnShow a una rutina que espera internamente.
  • Un botón Cancelar establece una bandera, pero el hilo principal sigue atascado en un bucle de espera.

Si los hilos de trabajo usan Synchronize durante ese tiempo, el deadlock está prácticamente garantizado. Con Queue también puede bloquearse si el hilo principal está detenido y no procesa mensajes —porque entonces la cola tampoco se vacía.

La consecuencia operativa es: el hilo principal no debe esperar de forma bloqueante al bucle paralelo si se requieren actualizaciones de UI en paralelo. En su lugar, el procesamiento debe delegarse por completo a una tarea en segundo plano, o bien organizar un «fin asíncrono» (callback / acción de finalización encolada) que libere la UI al final.

Enfoque limpio: progreso solo agregado, actualizaciones de UI limitadas

Motivo de escritorio con indicador de progreso abstraído y medición del tiempo como símbolo de actualizaciones de UI limitadas
La agregación y la limitación basada en tiempo evitan que la UI se inunde con demasiadas actualizaciones.

Un patrón robusto consiste en tres responsabilidades claramente separadas:

  • Worker-Threads realizan el trabajo real por elemento/índice. Informan solo del progreso en una forma segura para hilos (contador, Queue, Thread-safe Queue).
  • Aggregator (a menudo: hilo principal o un temporizador dedicado en la UI) calcula a partir del progreso un estado de UI (posición, texto, ETA) y actualiza los controles. Así evita actualizaciones 1:1 por iteración.
  • Finalización (también en el hilo principal): reactivar la UI, mostrar el resultado, resumir errores, liberar recursos.

Por qué esta separación funciona tan bien: la carga de trabajo puede ser de alta frecuencia (miles de elementos), pero la UI solo necesita pocas actualizaciones por segundo. En la práctica bastan 5–10 actualizaciones/segundo, y en trabajos muy rápidos incluso 2–4. Todo lo que supere eso suele ser solo ruido visual y consume tiempo de CPU en la Message Pump.

Contar seguro para hilos: atómico en lugar de Lock

Para una ProgressBar sencilla suele bastar un contador atómico. „Atómico“ significa: incremento y lectura ocurren sin condición de carrera, típicamente mediante TInterlocked. Con ello evita bloqueos (Critical Sections) en el hot path del bucle.

Idea mínima probada:

  • La cantidad total se conoce de antemano (p. ej., número de registros, archivos, IDs).
  • Cada iteración incrementa atómicamente un DoneCounter.
  • Un temporizador de UI lee periódicamente el contador y establece ProgressBar.Position.

Ventaja: ningún TThread.Queue por elemento, sin sobrecarga de la UI. Desventaja: no dispone de mensajes detallados por elemento (p. ej., nombre de archivo). Para eso se puede complementar con un segundo mensaje de estado limitado (ver siguiente sección).

Mensajes de estado sin spam: „el último estado gana“

Si además quiere mostrar un texto corto (elemento actual, fase, mensaje de error), también hace falta un patrón que no inunde la UI en cada paso del Worker. En la práctica funciona muy bien el principio „el último estado gana“:

  • El Worker escribe una información de estado en una estructura segura para hilos (p. ej., un String intercambiable de forma atómica, o protegido mediante un pequeño bloqueo).
  • Un temporizador de UI toma periódicamente el último estado visto y lo coloca en un Label.

Así la UI sigue siendo reactiva y aun así se percibe que „algo ocurre“. Lo importante aquí no es tanto el String en sí como su tiempo de vida: no transporte referencias a objetos efímeros de los hilos Worker al hilo de la UI. Si pasa objetos, aclare explícitamente la ownership.

TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster

Hay escenarios en los que un temporizador de UI por sí solo no basta: p. ej. cuando quiere garantizar exactamente una actualización de „Finalizado“ al final, o cuando la actualización de la UI es un paso más complejo (p. ej. una entrada en una ventana de log, pero limitada). Entonces TThread.Queue es apropiado, pero no por iteración, sino de forma selectiva.

Un enfoque práctico es una Queue solo para eventos de baja frecuencia:

  • Start-Event (preparar la UI, desactivar botones)
  • Eventos periódicos de progreso (máx. cada X milisegundos)
  • Eventos de error (opcionalmente agrupados)
  • Done-Event (restablecer la UI, mostrar el resultado)

La periodicidad no la controla el hilo de la UI, sino ya el contexto Worker: deje que los Worker encolen un UI-update (queuen) solo si ha pasado suficiente tiempo desde la última actualización de la UI. Para ello sirve una fuente de tiempo monótona (p. ej. TickCount) más un valor atómico de „last update“.

Importante: la actualización de la UI debe ser rápida. Cálculos costosos, I/O de archivos o accesos a base de datos no deben ir en el callback encolado a la UI. El callback solo debe leer estados y ajustar controles.

Cancel-Handling: Abbrechen ohne Hänger

En aplicaciones reales la cancelación no es opcional. Es crucial: Cancel no es un „Kill“, sino una terminación cooperativa. Los Worker deben comprobar periódicamente si se ha señalado la cancelación y, en ese caso, salir de forma limpia. En Delphi hay varias vías para esto (según la construcción PPL): una bandera Volatile propia, un Boolean atómico, o un concepto de cancelación sobre Tasks (según la versión y estructura de Delphi).

Para el funcionamiento son importantes dos reglas:

  • Cancelar debe ser visible rápidamente: Compruebe el flag de cancelación en puntos adecuados, no solo al final de una iteración, si la iteración puede durar varios segundos.
  • Cancelar debe limpiar: Los handles abiertos, archivos temporales, transacciones o locks no deben quedarse sin cerrar. Es decir: en cada iteración del worker son obligatorios los bloques try/finally cuando hay recursos involucrados.

En el lado de la UI, Cancel debería solo establecer una señal y poner la UI en un estado „Deteniendo…“. La finalización real y la reactivación de la UI ocurren luego en el evento Done, no inmediatamente al hacer clic.

Evitar deadlocks: las trampas más comunes en la práctica

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Los deadlocks suelen surgir por espera cíclica: el hilo de la UI se bloquea, los workers esperan acceso a la UI.

Trampa 1: WaitFor/Task.Wait en el hilo principal

Si el Main Thread se bloquea, no puede ejecutar callbacks de Queue ni procesar mensajes. Esto parece un deadlock, incluso si los workers continúan correctamente. Solución: no usar esperas bloqueantes en el hilo de la UI. En su lugar, realizar la acción final a través de TThread.Queue o mediante un control por eventos (p. ej., un Timer que compruebe „listo“).

Trampa 2: Synchronize dentro de un Lock

Un clásico: el worker mantiene una Critical Section, llama a Synchronize, y en el callback de la UI se necesita (directa o indirectamente) la misma Critical Section. Resultado: espera en círculo. La regla es sencilla: No pasar la UI (Synchronize/Queue) mientras se sostiene un lock. Si se necesita un lock, obtenga todos los datos primero en variables locales, salga del lock y luego encole.

Trampa 3: el callback de la UI provoca reentrancy

A veces la actualización de la UI en sí no es „inofensiva“: establecer propiedades puede disparar eventos (OnChange, OnResize) que a su vez lanzan lógica que accede a estados de los workers. Esto no es un deadlock en sentido estricto, pero provoca bloqueos difíciles de explicar y condiciones de carrera. Solución: colocar las actualizaciones de UI en rutas „silenciosas“ (desactivar eventos temporalmente) o usar Reentrancy-Guards (p. ej., un guard atómico para la fase de actualización).

Trampa 4: demasiadas actualizaciones encoladas

Incluso sin waits la UI puede „pararse“ si genera decenas de miles de callbacks encolados. Síntomas: la barra de progreso sigue actualizándose durante mucho tiempo, la ventana responde con lentitud, uso alto de CPU en el Main Thread. Solución: limitar (ventana temporal), agregar (contador), o una verdadera estructura Producer/Consumer en la que solo pueda haber una actualización de UI „pending“ (Coalescing).

Cuando se complica: recolectar resultados, agrupar errores, garantizar orden

TParallel.For es ideal cuando las iteraciones son independientes. En software empresarial las iteraciones suelen ser solo „parcialmente“ independientes: leen archivos, llaman a APIs REST, escriben filas en la base de datos. Entonces debe planificar tres puntos adicionales con cuidado:

  • Recolección de resultados segura para hilos: O bien un buffer local por hilo (fusionarlo al final) o una queue/collection segura para hilos. Evite locks en el hot path.
  • Manejo de errores: Las excepciones de los hilos de trabajo deben recopilarse. En la práctica funciona bien: guardar la primera excepción y activar Cancel, o recopilar todas las excepciones y mostrarlas al final agrupadas.
  • Orden: Si la salida requiere un orden estable (p. ej., logs por índice), el procesamiento paralelo con ordenación posterior suele ser más sencillo que la «inserción ordenada segura para hilos».

Para la UI esto significa: no muestre cada mensaje de error de inmediato. Eso conduce a un infierno de diálogos modales. Recopile los errores (p. ej., una lista de cadenas) y muestre al final un resumen o un registro exportable.

Depuración: Cómo hacer realmente visible el interbloqueo

Los interbloqueos en código paralelo son frustrantes, porque pueden verse distintos en el depurador que en el modo Release. Aun así, hay algunas palancas muy prácticas:

Usar la ventana de hilos y las pilas de llamadas

Si la UI se queda colgada, examine todos los hilos: ¿dónde está el hilo principal? ¿Está esperando? ¿Está en un bucle de mensajes? ¿Dónde están los hilos de trabajo? Si los workers se quedan en Synchronize, la causa es casi siempre «hilo principal bloqueado» o «hilo principal necesita un bloqueo».

Marcar los puntos de Queue/Synchronize

Inserte logging de forma selectiva en los puntos de transferencia (antes de la Queue, en el Queue-Callback, al final de la iteración). En producción eso suele ser más útil que los breakpoints, porque el timing es decisivo. Asegúrese de que el propio logging sea seguro para hilos y no bloqueante (p. ej., no emitir registros en la UI directamente desde los hilos de trabajo).

Medir el tiempo de la última actualización

Si la UI se «cuelga», puede ser simplemente que tenga que procesar demasiadas actualizaciones. Mida, por tanto, en el hilo principal cuántas actualizaciones de UI ejecuta por segundo y cuánto duran. En cuanto los callbacks de UI tarden más de unos pocos milisegundos, será necesario aplicar limitación o simplificación.

¿Cuándo merece la pena realmente TParallel.For con una UI de progreso?

La paralelización no es un fin en sí misma. Conviene especialmente cuando:

  • las iteraciones son lo bastante grandes (milisegundos a segundos), de modo que la sobrecarga del thread pool queda amortizada,
  • la tarea es intensiva en CPU (parsing, compresión, hashing) o tiene E/S bien paralelizables (varios archivos, múltiples solicitudes HTTP con límites),
  • dispone de una estrategia clara de Cancel y de manejo de errores,
  • los requisitos de la UI se conforman con un progreso agregado.

No es recomendable cuando cada iteración es extremadamente corta (microoperaciones) o cuando todas las iteraciones confluyen en el mismo cuello de botella (una transacción de BD serial, un bloqueo global, un único archivo). En esos casos la palanca más rápida suele ser: mejorar el algoritmo, procesar en lotes, reducir el acceso a datos o desacoplar explícitamente el cuello de botella.

Lista de verificación práctica: así se mantiene estable la UI

  • El hilo principal no debe bloquearse: nada de waits, ni bucles largos sin message pump.
  • Los workers nunca deben tocar los Controls: no acceder a VCL/FMX fuera del hilo de UI.
  • Las actualizaciones de UI están limitadas: contadores/timers o actualizaciones agrupadas de la cola en vez de por iteración.
  • No realizar llamadas a Synchronize desde dentro de locks.
  • Cancel es cooperativo, se comprueba con frecuencia y limpia correctamente.
  • Las excepciones se recopilan y se tratan ordenadas al final.

Conclusión: desacoplar con cola, estabilizar con agregación

Una interfaz de progreso robusta con TParallel.For no se consigue con «meter un Synchronize en cualquier sitio», sino con un principio arquitectónico claro: los Worker trabajan de forma independiente, el Main Thread queda libre y solo procesa unas pocas actualizaciones de UI rápidas. TThread.Queue es la herramienta adecuada si la utiliza de forma controlada y limitada. El comportamiento «lento» o inestable suele deberse casi siempre a dos causas: el Main Thread espera bloqueado en algún punto — o se ahoga por un exceso de actualizaciones encoladas.

Si configura el patrón correctamente (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), merece la pena aplicarlo en muchos puntos de una aplicación Delphi ya consolidada: importación/exportación, validaciones de datos, trabajos de archivo y de API — todo será más reactivo, sin que por cada actualización de progreso se arriesgue a nuevos deadlocks.

Si necesita apoyo para estabilizar código paralelo, depurar bloqueos de la UI o modernizar de forma ordenada aplicaciones Delphi heredadas: póngase en contacto.

Para este tema son también relevantes la Delphi Parallel Programming Library y Tthread.queue Vs Synchronize. El artículo contextualiza estos aspectos de forma comprensible y muestra en qué hay que fijarse en el día a día.

Hablar sobre un proyecto o iniciativa de modernización con Net-Base.

siguiente paso

Cuando un tema se convierte en un proyecto real, arquitectura, entorno existente y operación deben considerarse conjuntamente desde el inicio.

No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

  • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
  • REST, el acceso a datos, los portales y el despliegue no se relegan a fases posteriores.
  • Usted detecta con antelación qué camino es viable, tanto económica como operativamente.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y el texto breve directamente.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.