Net-Base Revista

24.07.2026

Delphi: TParallel.For com interface de progresso segura para threads (TThread.Queue) sem deadlocks

Como combinar Delphi TParallel.For com uma UI de progresso thread-safe: atualizações via TThread.Queue, agregação limpa, tratamento de cancelamento e armadilhas típicas de deadlock na depuração e em operação.

24.07.2026

Do tema da revista à prática do projeto

Páginas de serviços e técnicas correspondentes ao artigo

Quem em Delphi quiser paralelizar tarefas intensivas em CPU ou I/O, acaba rapidamente na Parallel Programming Library (PPL) e, mais concretamente, em TParallel.For. O efeito é frequentemente mensurável de imediato — até que se tente, no mesmo fôlego, atualizar uma Progress-UI „rapidinho“. É aí que surgem os travamentos típicos: congelamentos de UI aparentemente aleatórios, uma ProgressBar que salta para trás, ou um deadlock completo assim que se avança passo a passo no depurador.

Neste artigo trata-se de TParallel.For threadsichere Progress-UI: um padrão robusto que funciona em VCL e FMX, agrupa de forma limpa as atualizações de UI via TThread.Queue, leva em conta Cancel/Abort e evita de forma consistente as armadilhas de deadlock mais comuns. O foco é a realidade operacional: comportamento reproduzível, responsabilidades compreensíveis e dicas de debugging que ajudam mesmo quando o erro só ocorre “no cliente”.

Warum Progress-UI bei TParallel.For so oft schiefgeht

TParallel.For roda tipicamente em worker-threads do pool de threads Delphi. Esses threads não devem manipular controles VCL ou FMX diretamente, porque os frameworks de UI (message loop, window handles, rendering) estão ligados ao Main Thread. Mesmo um aparentemente inofensivo ProgressBar.Position := … vindo de um worker pode levar a comportamento indefinido: AVs esporádicos, janelas congeladas ou atualizações “piscantes”.

A correção óbvia costuma ser TThread.Synchronize. Isso resolve a thread-safety, mas em loops paralelos rapidamente conduz a outro problema: cria um gargalo serial. Cada worker espera pelo UI-Thread, que por sua vez está ocupado com renderização e com o processamento das chamadas Synchronize. Sob carga isso parece um deadlock — mesmo quando é “apenas” um efeito de starvation/lockstep.

E há também a classe de deadlock verdadeira: o Main Thread espera (por exemplo via WaitFor, Task.Wait ou indiretamente por chamadas bloqueantes) pelo fim da operação paralela, enquanto os worker-threads tentam enviar trabalho para o Main Thread via Synchronize ou uso imprudente de Queue. Resultado: Main Thread espera por workers, workers esperam pelo Main Thread.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
Ao depurar é fácil ver se os worker-threads estão esperando pelo Main Thread ou apenas enfileirando atualizações.

Ambos os mecanismos servem para executar código com segurança no Main Thread. A diferença está na semântica de espera:

  • TThread.Synchronize: o worker que chama espera até que o Main Thread execute o código. Isso é “síncrono”, aumenta latências e é um ingrediente clássico para deadlocks quando o Main Thread está bloqueado.
  • TThread.Queue: O Worker coloca o código apenas numa Warteschlange para den Main Thread e continua a correr. Das ist „asynchron“, entkoppelt Threads und ist in Parallel-Szenarien fast immer die bessere Default-Wahl – sofern man die Update-Frequenz kontrolliert.

Wichtig: Queue ist kein Freifahrtschein. Wenn Sie in jeder Iteration einer Schleife ein Queue-Update schicken, überfluten Sie die Main-Thread-Queue. Dann hängt die UI zwar nicht wegen Deadlock, aber wegen schierer Menge an Nachrichten. Die UI wirkt „zäh“, und das Ende der Verarbeitung verzögert sich, weil noch hunderte oder tausende UI-Updates nachlaufen.

Der Randfall, der wirklich wehtut: Warten im UI-Thread

In Unternehmensanwendungen sieht man häufig folgenden Ablauf: Button „Start“ klickt, UI wird deaktiviert, ProgressDialog geht auf, dann wird synchron „gewartet“, bis alles fertig ist, um anschließend wieder zu aktivieren. Dieses Muster ist der Kern vieler Deadlocks.

Typische Varianten (je nach Codebasis):

  • Main Thread startet TParallel.For und ruft danach eine blockierende Wartelogik auf (direkt oder indirekt).
  • Ein ProgressDialog ruft im Constructor oder OnShow eine Routine auf, die intern wartet.
  • Ein Abbrechen-Button setzt zwar ein Flag, aber der Main Thread bleibt trotzdem in einem Warteloop hängen.

Wenn Worker-Threads in dieser Zeit Synchronize benutzen, ist der Deadlock praktisch garantiert. Mit Queue kann es ebenfalls hängen, wenn der Main Thread blockiert und keine Messages pumpt – denn dann wird auch die Queue nicht abgearbeitet.

Die betriebsfeste Konsequenz lautet: Der Main Thread darf nicht blockierend auf die Parallel-Schleife warten, wenn parallel UI-Updates benötigt werden. Stattdessen muss die Verarbeitung entweder vollständig in einen Hintergrund-Task ausgelagert werden, oder man organisiert ein „asynchrones Ende“ (Callback/Queued Abschlussaktion), das die UI am Schluss wieder freigibt.

Sauberer Ansatz: Progress nur aggregiert, UI-Updates gedrosselt

Arbeitsplatzmotiv mit abstrahierter Progress-Anzeige und Zeitmessung als Symbol für gedrosselte UI-Updates
A agregação e a limitação baseada no tempo evitam que a UI seja inundada por atualizações em excesso.

Ein robustes Muster besteht aus drei klar getrennten Verantwortlichkeiten:

  • Worker-Threads führen die eigentliche Arbeit pro Element/Index aus. Sie melden nur Fortschritt in einer threadsicheren Form (Zähler, Queue, Thread-safe Queue).
  • Aggregator (oft: Main Thread oder ein dedizierter Timer im UI) berechnet aus dem Fortschritt einen UI-Status (Position, Text, ETA) und aktualisiert Controls. So vermeiden Sie 1:1-Updates pro Iteration.
  • Abschluss (ebenfalls im Main Thread): UI reaktivieren, Ergebnis anzeigen, Fehler zusammenfassen, Ressourcen freigeben.

Por que essa separação funciona tão bem: a carga de trabalho pode ser de alta frequência (milhares de elementos), mas a UI precisa de apenas algumas atualizações por segundo. Na prática 5–10 atualizações/segundo são suficientes, em jobs muito rápidos até 2–4. Tudo acima disso normalmente é apenas ruído visual e consome tempo de CPU na Message Pump.

Contagem segura para threads: atômico em vez de lock

Para uma ProgressBar simples, um contador atômico costuma ser suficiente. „Atômico“ significa: incremento e leitura ocorrem sem Race Condition, tipicamente via TInterlocked. Assim você evita locks (Critical Sections) no hot path do loop.

Ideia mínima comprovada:

  • O total é conhecido antecipadamente (p. ex. número de registros, arquivos, IDs).
  • Cada iteração incrementa de forma atômica um DoneCounter.
  • Um timer da UI lê o contador periodicamente e define ProgressBar.Position.

Vantagem: Nenhum TThread.Queue por elemento, sem saturação da UI. Desvantagem: não há mensagens detalhadas por elemento (p. ex. nome de arquivo). Para isso pode-se acrescentar uma segunda mensagem de status limitada (veja a próxima seção).

Mensagens de status sem spam: „o último status vence“

Se você também quer mostrar um texto curto (elemento atual, fase, mensagem de erro), é preciso um padrão que não inunde a UI a cada passo do worker. Na prática, o „o último status vence“ funciona muito bem:

  • O worker escreve uma informação de status em uma estrutura segura para threads (p. ex. string substituível de forma atômica, ou protegida por um pequeno lock).
  • Um timer da UI atualiza periodicamente um rótulo com o último status observado.

Assim a UI permanece responsiva, e você ainda vê que „algo está acontecendo“. Aqui é menos importante a string em si do que seu tempo de vida: não arraste referências a objetos de curta duração dos worker-threads para o thread da UI. Se passar objetos, esclareça a propriedade explicitamente.

TParallel.For UI de progresso thread-safe com TThread.Queue: um padrão robusto

Existem cenários nos quais um timer da UI sozinho não é suficiente: p. ex. quando você quer garantir exatamente uma atualização „Concluído“ no fim, ou quando a atualização da UI é um passo mais complexo (p. ex. inserir em uma janela de log, porém com limitação). Nesse caso TThread.Queue é adequado – mas não por iteração, e sim de forma seletiva.

Uma abordagem prática é uma fila apenas para eventos de baixa frequência:

  • Evento de início (preparar UI, bloquear botões)
  • Eventos periódicos de progresso (no máximo a cada X milissegundos)
  • Eventos de erro (opcionalmente acumulados)
  • Evento Done (restaurar UI, exibir resultado)

A periodicidade não é alcançada pelo thread da UI, mas já no contexto do worker: permita que o worker faça queue de uma atualização de UI apenas se tempo suficiente tiver passado desde a última atualização. Para isso serve uma fonte de tempo monótona (p. ex. TickCount) mais um valor atômico de „última atualização“.

Importante: a própria atualização da UI deve ser „rápida“. Cálculos caros, I/O de arquivos ou acessos a banco de dados não pertencem ao callback da UI enfileirado. O callback deve apenas ler estados e ajustar controles.

Tratamento de cancelamento: cancelar sem travamentos

Em aplicações reais, cancelar não é opcional. O essencial é: Cancel não é um „kill“, mas uma terminação cooperativa. Os workers devem verificar regularmente se um sinal de cancelamento foi acionado e então sair de forma limpa. Em Delphi existem várias vias para isso (dependendo da construção PPL): uma flag Volatile própria, um booleano atômico, ou um conceito de cancelamento via tasks (dependendo da versão e da estrutura de Delphi).

Para a operação são importantes duas regras:

  • Cancel muss schnell sichtbar werden: Verifique a flag de cancelamento em pontos sensatos, não apenas no final de uma iteração, quando a iteração por vezes pode durar segundos.
  • Cancel muss aufräumen: Handles abertos, arquivos temporários, transações ou Locks não devem permanecer. Ou seja: em cada Worker-Iteração são obrigatórios blocos try/finally, quando recursos estão envolvidos.

Do lado da UI, o Cancel deve apenas definir um sinal e colocar a UI em um estado „Stopping…“. A terminação efetiva e a reativação da UI ocorrem no Done-Event, não imediatamente ao clicar.

Evitar deadlocks: as armadilhas mais comuns na prática

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks costumam ocorrer por espera cíclica: UI-Thread bloqueado, Worker aguardando acesso à UI.

Armadilha 1: WaitFor/Task.Wait im Main Thread

Quando o Main Thread fica bloqueado, ele não consegue executar callbacks de Queue nem processar mensagens. Isso aparenta um deadlock, mesmo que os Worker continuem a correr corretamente. Solução: não use waits bloqueantes no UI-Thread. Em vez disso, execute a ação de conclusão via TThread.Queue ou por controle de evento (p. ex. um Timer verifica „pronto“).

Armadilha 2: Synchronize innerhalb eines Locks

Um clássico: o Worker mantém uma Critical Section, chama então Synchronize, e no callback da UI é necessária (direta ou indiretamente) de novo a mesma Critical Section. Resultado: espera circular. A regra é simples: Nenhuma passagem para a UI (Synchronize/Queue) a partir de um Lock mantido. Se um Lock for necessário, copie todos os dados para variáveis locais, libere o Lock e só então chame o Queue.

Armadilha 3: UI-Callback triggert Reentrancy

Por vezes a atualização da UI não é «inócua»: definir Properties pode disparar Events (OnChange, OnResize), que por sua vez acionam lógica que acessa estados dos Worker. Isso não é um deadlock no sentido estrito, mas leva a bloqueios de difícil explicação e a Race Conditions. Abordagem: colocar atualizações de UI em caminhos „silenciosos“ (desativar Events temporariamente) ou usar Reentrancy-Guards (p. ex. um guard atômico para a fase de atualização).

Armadilha 4: Zu viele queued Updates

Mesmo sem Waits, a UI pode «parar» se você gerar dezenas de milhares de callbacks queued. Sintomas: a ProgressBar continua por muito tempo, a janela responde de forma lenta, CPU do Main Thread alta. Solução: limitar (janela de tempo), agregar (contador), ou uma verdadeira estrutura Producer/Consumer em que apenas uma atualização de UI possa estar „pending“ (Coalescing).

Quando fica mais complicado: coletar resultados, agrupar erros, garantir a ordem

TParallel.For é ideal quando as iterações são independentes. Em software empresarial, contudo, as iterações frequentemente são apenas „amplamente“ independentes: leem arquivos, chamam APIs REST, escrevem linhas no banco de dados. Nesse caso, você precisa planear cuidadosamente três pontos adicionais:

  • Thread-sichere Ergebnis-Sammlung: Ou por thread um buffer local (mesclar no fim) ou uma Queue/Collection thread-safe. Evitar Locks no Hot Path.
  • Tratamento de erros: exceções de worker-threads devem ser recolhidas. Na prática funciona bem: memorizar a primeira exceção e acionar o cancelamento, ou coletar todas as exceções e exibi-las resumidas ao final.
  • Ordem: se a saída precisa de ordem estável (p. ex. logs por índice), o processamento paralelo com ordenação posterior é frequentemente mais simples do que um “inserir ordenado thread-safe”.

Para a UI isso significa: não mostre cada mensagem de erro imediatamente. Isso leva ao inferno dos diálogos modais. Recolha erros (p. ex. uma lista de strings) e mostre no final um resumo ou um log exportável.

Debugging: Como tornar o deadlock realmente visível

Deadlocks em código paralelo são frustrantes porque no depurador podem parecer diferentes do que em Release. Ainda assim existem algumas alavancas práticas:

Usar a janela de threads e as stacks de chamada

Se a UI travar, examine todos os threads: onde está o thread principal? Está aguardando? Está num loop de mensagens? Onde estão os worker-threads? Se os workers estiverem presos em Synchronize, a causa quase sempre é “thread principal bloqueado” ou “thread principal precisa de um lock”.

Marcar pontos de fila/Synchronize

Coloque logging direcionado nos pontos de passagem (antes da fila, no callback da fila, ao fim da iteração). Em produção isso costuma valer mais do que breakpoints, porque o timing é decisivo. Garanta que o próprio logging seja thread-safe e não bloqueante (p. ex. não emitir logs na UI diretamente a partir dos worker-threads).

Medir o tempo desde o último update

Se a UI “trava”, pode ser simplesmente que ela tenha de processar demasiadas atualizações. Meça, portanto, no thread principal com que frequência por segundo você executa um update de UI e quanto tempo ele demora. Assim que callbacks de UI consumirem mais do que alguns milissegundos, é necessário limitar ou simplificar.

Quando vale a pena usar TParallel.For com Progress-UI?

Paralelizar não é um fim em si. Vale especialmente quando:

  • as iterações são suficientemente grandes (milissegundos a segundos), de modo que o overhead do threadpool seja amortizado,
  • a tarefa é intensiva em CPU (parsing, compressão, hashing) ou tem I/O bem paralelizável (vários ficheiros, múltiplos HTTP-requests com limites),
  • você tem uma estratégia clara de cancelamento e de tratamento de erros,
  • os requisitos da UI aceitam progresso agregado.

Vale menos a pena quando cada iteração é extremamente curta (micro-operações) ou quando todas as iterações concorrem pelo mesmo gargalo (uma transação DB serial, um lock global, um único ficheiro). Nesse caso a alavanca mais rápida costuma ser: melhorar o algoritmo, processar em lotes (batching), reduzir o acesso a dados ou desacoplar explicitamente o gargalo.

Checklist prática: Como manter a UI estável

  • Thread principal não bloqueado: sem waits, sem loops longos sem processamento de mensagens.
  • Workers nunca tocam Controls: sem acessos VCL/FMX fora do thread da UI.
  • UI-updates são limitados: contadores/timers ou atualizações agrupadas via fila em vez de por iteração.
  • Sem chamadas Synchronize a partir de locks.
  • O cancelamento é cooperativo, verificado frequentemente e faz limpeza adequada.
  • Exceções são coletadas e tratadas ordenadamente ao final.

Conclusão: desacoplar com fila, estabilizar com agregação

Uma interface de progresso robusta em TParallel.For não surge por „enfiar um Synchronize em qualquer lugar“, mas por um princípio arquitetural claro: Workers trabalham de forma independente, o Main Thread permanece livre e processa apenas poucos e rápidos UI-Updates. TThread.Queue é a ferramenta correta quando usada de forma direcionada e controlada. Para comportamento „lento“ ou instável, quase sempre há duas causas: o Main Thread está bloqueado em algum ponto – ou ele se afoga em atualizações enfileiradas em excesso.

Quando você montar o padrão corretamente (Counter/Coalescing, Cancel-Flag, callback de conclusão), vale a pena aplicá-lo em muitos pontos de uma aplicação Delphi consolidada: import/export, validações de dados, tarefas de ficheiros e jobs de API – tudo fica mais responsivo, sem que você provoque novos deadlocks a cada atualização de progresso.

Se precisar de suporte para estabilizar código paralelo, depurar travamentos de UI ou modernizar de forma limpa aplicações Delphi legadas: entre em contato.

Para este tema também são importantes a Delphi Parallel Programming Library e Tthread.queue Vs Synchronize. O artigo contextualiza esses aspectos de forma clara e mostra no dia a dia o que realmente importa.

Discutir projeto ou iniciativa de modernização com Net-Base.

Próximo passo

Quando o tema se tornar um projeto real, arquitetura, ambiente existente e operação devem ser considerados em conjunto desde o início.

Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.

  • Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
  • REST, o acesso a dados, os portais e o Rollout não são adiados para fases posteriores.
  • Você identifica cedo qual caminho é viável econômica e operacionalmente.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e E‑Mail estão disponíveis imediatamente. Para o Instagram, preparamos diretamente o link e o texto curto.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.