Quem trabalha com Threads em Delphi acaba mais cedo ou mais tarde em TThread.Synchronize. E é exatamente aí que surgem os problemas desagradáveis: travamentos esporádicos, “UI não responde”, deadlocks aparentemente aleatórios ao encerrar ou ao abrir uma caixa de diálogo. A raiz raramente é “Delphi está quebrado”; quase sempre trata‑se de uma combinação desfavorável de Synchronize, operações de espera bloqueantes e um UI‑Thread que deixa de processar corretamente sua Message Loop (o processamento de eventos da VCL). Este artigo mostra padrões robustos e praticáveis no contexto legacy para TThread e Synchronize sem deadlocks na UI — incluindo variante com timeout, passagem de erro limpa, regras de shutdown e dicas de depuração que ajudam em aplicações de produção existentes.
Por que deadlocks em torno de Synchronize acontecem na prática
Synchronize significa: uma thread de trabalho coloca uma procedure numa fila que será executada no Main Thread e normalmente aguarda até essa procedure terminar. Em aplicações VCL o Main Thread é também o UI‑Thread (janelas, controles, eventos). Além disso, em muitas instalações há objetos COM rodando no STA‑Modell (Single‑Threaded Apartment: chamadas COM precisam ser processadas no mesmo thread), o que aumenta ainda mais a dependência de uma Message Loop funcional.
Deadlocks surgem tipicamente por uma destas constelações:
- WaitFor no Main Thread: o UI‑Thread espera por um worker (por exemplo
MyThread.WaitFor), enquanto o worker precisa do UI‑Thread viaSynchronize. Ambos ficam esperando — fim de jogo. - Lock‑Inversion: o worker segura um lock (por exemplo
TCriticalSectionouTMonitor) e chamaSynchronize. A procedure sincronizada na UI tenta adquirir o mesmo lock (direta ou indiretamente, frequentemente via logging/cache/singletons) — deadlock clássico. - Shutdown/Destroy: ao fechar uma form uma thread é terminada enquanto ainda há chamadas
Synchronizependentes. Especialmente problemático: chamadas sincronizadas referenciam controles que estão sendo destruídos. - Message Loop bloqueada: diálogos modais, operações longas na UI, uma chamada COM bloqueante ou um handler que “só rapidinho” faz DB/REST prendem o Main Thread. Chamadas
Synchronizesão processadas tardiamente ou nem chegam a ser processadas.
A consequência arquitetural e operacional mais importante: Synchronize é uma aresta bloqueante. Em software empresarial personalizado com importações, BDE‑Ablosung com ligação nativa‑queries, jobs de interface ou serviços em background com componente UI essa aresta deve ser controlada conscientemente — caso contrário o “raro” vira “sempre que há pressa”.
Regra básica: nunca deixar o UI‑Thread esperar por um worker (quando Synchronize está em uso)
Se um worker usa Synchronize em qualquer lugar, o Main Thread não deve bloquear duramente esperando por esse worker. Isso parece trivial, mas em código legacy é uma das causas mais frequentes, porque “vamos esperar um pouco ao fechar” ou “o diálogo de progresso espera pelo fim” é inserido rapidamente.
Consequências práticas:
- Nenhuma chamada
WaitForno UI-Thread assim que existir no Worker um caminho que utilizeSynchronize. - Sinalizar o encerramento da thread via event/callback: a UI permanece responsiva e só realiza a limpeza após o sinal.
- Atualizações de UI, em regra, postar via
TThread.Queueou um dispatcher, para que o Worker não bloqueie.
TThread.Queue é frequentemente a melhor opção padrão: o Worker posta trabalho para o Main Thread, continua executando e não bloqueia. Isso previne muitos deadlocks. Porém não resolve todos os casos-limite – por exemplo quando você, em um Worker, necessariamente precisa de um resultado gerado no Main Thread (p. ex. acesso a um recurso vinculado à UI ou a um componente com afinidade de thread).
TThread und Synchronize ohne UI-Deadlocks: Denkmodell für saubere Übergaben
Um modelo mental robusto é: existem poucas passagens síncronas legítimas para o Main Thread. Todo o resto é estado, apresentação ou telemetria – e, portanto, assíncrono.
Uma classificação simples ajuda em revisões e na estabilização de projetos existentes:
- „Apenas exibir“: progresso, linha de log, contador, semáforo, habilitar/desabilitar – sempre
Queue. - „Transferir estado“: o Worker entrega um objeto de dados/DTO, a UI renderiza –
Queue, mas com cópia/imutabilidade (ou seja, sem estruturas mutadas em comum). - „A UI precisa decidir“: só aqui você precisa de semântica síncrona (p. ex. uma pergunta ao usuário). A questão real é: o Worker precisa realmente esperar, ou o fluxo pode ser reestruturado (máquina de estados, cancelar o job, retomar depois)?
Justamente a terceira categoria é uma armadilha de deadlock: se o Worker aguarda um resultado da UI, a UI tende a esperar pelo Worker (ou indiretamente via locks). Isso falha mais rapidamente sob carga, com bancos de dados lentos ou em ambientes de Remote Desktop.
Source-Schnipsel: UI-Dispatcher mit Queue, optionalem Timeout und sauberem Shutdown
O padrão abaixo encapsula as passagens para a UI em uma pequena classe utilitária. Você obtém:
- Post: fire-and-forget via
TThread.Queue(típico para atualizações de status). - Call: chamada síncrona com Timeout (incomum, mas útil em situações legacy), sem usar diretamente
Synchronizecomo ponto de bloqueio. - Proteção contra shutdown: não aceitar novos UI-jobs e fazer com que jobs enfileirados verifiquem um flag antes de tocar nos controles.
Enquadramento técnico: usamos Queue mais TEvent (um evento de kernel) para feedback. O Worker não espera por Synchronize, mas por um evento que é sinalizado no Main Thread depois que a ação enfileirada for executada. O timeout evita um bloqueio “eterno” caso o UI-Thread, por algum motivo, deixe de processar as ações.
unit UiDispatch;
interface
uses
System.SysUtils,
System.Classes,
System.SyncObjs;
type
EUiDispatchTimeout = class(Exception);
EUiDispatchShuttingDown = class(Exception);
/// <summary>
/// Kapselt UI-Aufrufe aus Worker-Threads.
/// Post: asynchron (Queue).
/// Call: synchron mit Timeout, ohne TThread.Synchronize direkt zu blocken.
/// </summary>
TUiDispatcher = class
strict private
class var FShuttingDown: Integer;
public
class procedure BeginShutdown; static;
class function IsShuttingDown: Boolean; static;
class procedure Post(const AProc: TProc); static;
class procedure Call(const AProc: TProc; ATimeoutMs: Cardinal = 5000); static;
end;
implementation
{ TUiDispatcher }
class procedure TUiDispatcher.BeginShutdown;
begin
TInterlocked.Exchange(FShuttingDown, 1);
end;
class function TUiDispatcher.IsShuttingDown: Boolean;
begin
Result := TInterlocked.CompareExchange(FShuttingDown, 0, 0) = 1;
end;
class procedure TUiDispatcher.Post(const AProc: TProc);
begin
if not Assigned(AProc) then
Exit;
// Im Shutdown keine neuen UI-Jobs mehr annehmen.
if IsShuttingDown then
Exit;
// Queue blockiert den Worker nicht.
TThread.Queue(nil,
procedure
begin
if IsShuttingDown then
Exit;
AProc();
end);
end;
class procedure TUiDispatcher.Call(const AProc: TProc; ATimeoutMs: Cardinal);
var
DoneEvent: TEvent;
RaisedObj: TObject;
begin
if not Assigned(AProc) then
Exit;
if IsShuttingDown then
raise EUiDispatchShuttingDown.Create('UI-Dispatcher ist im Shutdown.');
DoneEvent := TEvent.Create(nil, True, False, '');
try
RaisedObj := nil;
TThread.Queue(nil,
procedure
begin
try
if not IsShuttingDown then
AProc();
except
// Exception-Objekt über die Thread-Grenze reichen.
// Achtung: Kein "raise" hier, sonst landet es im Main Thread.
RaisedObj := AcquireExceptionObject;
end;
DoneEvent.SetEvent;
end);
case DoneEvent.WaitFor(ATimeoutMs) of
wrSignaled:
begin
if Assigned(RaisedObj) then
raise Exception(RaisedObj);
end;
wrTimeout:
raise EUiDispatchTimeout.CreateFmt(
'Timeout nach %d ms: Main Thread hat UI-Aufruf nicht abgearbeitet.',
[ATimeoutMs]);
else
raise Exception.Create('Unerwarteter WaitFor-Status im UI-Dispatcher.');
end;
finally
DoneEvent.Free;
end;
end;
end.Objetivo do código e onde ele é deliberadamente „incomum“
O padrão não substitui completamente o Synchronize, mas torna as chamadas síncronas controláveis: o worker não espera pelo mecanismo de Synchronize, mas por um evento. Isso permite impor timeouts, detectar em operação que a thread de UI está travada e recusar de forma consistente novos jobs de UI durante a fase de shutdown.
A parte „incomum“ não é o evento, mas a decisão de representar a semântica síncrona com Queue + Event. Isso vale a pena precisamente quando é necessário melhorar a estabilidade gradualmente em aplicações legadas, sem ter que refatorar arquitetonicamente todos os pontos que usam Synchronize de imediato.
Condições e armadilhas
- Visibilidade de memória:
DoneEventé a borda de sincronização. Isso garante que a leitura deRaisedObjapósWaitForseja consistente. Ainda assim,RaisedObjdeve permanecer local por chamada (como aqui), nunca global.
AcquireExceptionObject impede que a exceção „desapareça“ no thread principal. Ao relançar no Worker, o stacktrace não é idêntico à origem, mas a mensagem de erro permanece no log do Worker, e o job pode falhar de forma limpa.BeginShutdown pertence a uma sequência central de shutdown (p.ex., bem cedo em OnCloseQuery do formulário principal). Caso contrário, ainda serão enfileirados UI-jobs enquanto janelas já estiverem destruídas.Estratégia de bloqueios: como evitar inversões de bloqueio com callbacks de UI
Muitos deadlocks não surgem por WaitFor, mas por uma ordem de bloqueios pouco clara. Fluxo típico: o Worker bloqueia o „modelo de dados“, chama a atualização de UI via Synchronize, a atualização de UI acessa novamente o „modelo de dados“. Isso é logicamente compreensível, mas tecnicamente fatal.
Regras práticas que podem ser adotadas por equipes:
- Não manter bloqueios além dos limites de thread: Antes de um Worker enfileirar/sincronizar algo em direção à UI, os bloqueios de domínio devem ser liberados.
- UI lê snapshots: Callbacks de UI não devem olhar „ao vivo“ para estruturas do Worker, mas exibir cópias/snapshots (p.ex., DTO, Record, valores simples).
- Logging é um candidato a bloqueio: Se o logging usa internamente uma fila, bloqueio de arquivo ou um singleton, pode fazer parte de um deadlock. Callbacks de UI devem manter o logging ao mínimo ou escrever por meio de uma pipeline de log separada e não bloqueante.
Se você já tem uma arquitetura Layer-3 (UI, Services/Domäne, infraestrutura como acesso a dados): callbacks de UI idealmente devem fazer apenas UI. Tudo o que for „Service“ não pertence ao callback. Isso reduz significativamente efeitos de reentrância.
Shutdown sem travamentos: „não WaitFor, mas parada cooperativa“
No encerramento costuma dar problema: a UI fecha, um thread deve encerrar, mas UI-jobs enfileirados ainda estão abertos. Um shutdown limpo é menos „matar threads“ e mais uma pequena coreografia:
- Definir flag de shutdown (p.ex.,
TUiDispatcher.BeginShutdown): A partir de agora, nenhum novo UI-job deve ser aceito. - Parar os Workers de forma cooperativa: O Worker verifica um cancel-flag (p.ex.,
TEventou similar aTCancellationToken) e encerra loops/esperas. - Não bloquear a UI: Nenhum loop duro de espera no thread principal. Se precisar „esperar“, faça-o apenas com a message loop em funcionamento (ou melhor: evitar totalmente, tratando a conclusão via callback).
- Últimas limpezas da UI somente se janelas/controles garantidamente ainda existirem. Na VCL o timing é importante: assim que o handle sumir, jobs enfileirados não devem mais acessar controles.
Esse procedimento é relevante para operação e suporte: „A aplicação trava ao fechar“ é um problema clássico de aceitação, embora funcionalmente tudo tenha sido processado corretamente. Um shutdown definido economiza tempo real aqui.
Depuração: Como tornar o deadlock palpável (sem adivinhações)
Quando há um bloqueio, a pergunta central é: Quem está esperando em quem? Algumas abordagens que se mostraram eficazes em projetos existentes:
- Inventariar todos os pontos de espera: Pesquisa em texto completo por
WaitFor,Sleepem loops,TEvent.WaitFor,INFINITE. Muitos problemas são esperas ocultas (também em bibliotecas). - Estado da thread no log: Registre nos limites de thread: „Job iniciado“, „UI enfileirada“, „UI executada“, „Job concluído“. Assim você vê se o thread principal realmente processa jobs enfileirados.
- Verificar suspeita da Message Loop: Se o travamento ocorre apenas em diálogos modais ou em certas interações COM, a Message Loop costuma ser o gargalo. O objetivo então é: aliviar os manipuladores de UI, isolar chamadas COM, não executar operações longas na UI.
- Tornar locks visíveis: Para
TCriticalSection/TMonitorvale a pena um build de debug com metadados de “Owner” (p. ex. ID da thread no Enter) e medição temporal. Assim você vê qual lock o thread principal mantém enquanto os threads de trabalho aguardam pela UI.
O importante é a postura: deadlocks raramente são “acidentais”. São ciclos determinísticos que raramente são desencadeados. Uma vez que você tenha identificado o ciclo de forma limpa, a correção normalmente fica clara.
Variantes para acesso a dados e jobs de interface (FireDAC, REST, sistema de arquivos)
Especialmente com FireDAC (ou outros acessos a DB) aplica-se: conexão, transação e datasets são na prática vinculados à thread. Um thread de trabalho deve possuir exclusivamente seu próprio contexto de DB. Chamadas da UI devem se limitar à apresentação, não a operações de DB. Um padrão robusto é:
- O thread de trabalho executa a query/chamada REST, calcula o resultado e gera um DTO.
- O thread de trabalho posta o DTO via
Queue/TUiDispatcher.Postpara a UI. - A UI assume o DTO e atualiza os controles (sem recorrer a objetos do thread de trabalho).
Se você tem formas mistas herdadas („UI aciona DB, callback do DB aciona UI“), vale a pena uma desacoplagem gradual: primeiro isolar pontos de passagem (Dispatcher), depois deslocar estados para Services/Model. Isso é menos arriscado que uma grande reestruturação, mas reduz deadlocks de forma perceptível.
Conclusão: Evitar deadlocks significa controlar as transferências
TThread e Synchronize sem deadlocks de UI é menos uma técnica isolada do que uma disciplina: minimizar bloqueios, manter a ordem de locks limpa, definir o shutdown e reduzir dependências síncronas da UI. O UI-Dispatcher mostrado é particularmente útil em cenários legados, porque usa Queue como padrão, mas adiciona Timeout e regras claras de shutdown para transferências síncronas necessárias.
Existem limites de aplicação: se o thread principal ficar bloqueado de forma persistente (por lógica pesada de UI, cadeias de diálogos modais ou chamadas COM-STA), um Dispatcher apenas consegue diagnosticar e abortar de forma controlada. A solução sustentável é então aliviar a UI e separar responsabilidades. Se precisar de suporte para isso em uma aplicação existente Delphi – desde armadilhas de threading até estabilização gradual – você pode enquadrar o projeto aqui: discutir projeto ou iniciativa de modernização com Net-Base.
No contexto técnico, Delphi Multithreading e deadlocks por Synchronize também desempenham papel importante quando integrações, fluxos de dados e evolução precisam atuar de forma coordenada.
Discutir projeto ou iniciativa de modernização com Net-Base.