1. Introdução
O mundo do desenvolvimento moderno exige aplicações cada vez mais rápidas e responsivas. Com a popularização de processadores multi-core, executar tarefas simultaneamente deixou de ser modismo e tornou-se necessidade.
No Delphi, o suporte a threads existe desde muito tempo, mas trabalhar com threads no Delphi ou em qualquer outra linguagem exige cuidado. É necessário gerenciar sincronizações, lidar com exceções, manter a responsividade da UI, entre outras questões. Tudo isso pode transformar um código simples em algo muito mais complexo.
"The biggest problem with Thread is that it doesn't enforce the use of any programming patterns. Because of that, you can use it to create parallel programs that are hard to understand, hard to debug, and which work purely by luck. I should know - I shudder every time I have to maintain my old Thread-based code."
"O maior problema com Thread é que ela não impõe o uso de nenhum padrão de programação. Por causa disso, você pode usá-la para criar programas paralelos que são difíceis de entender, difíceis de depurar e que funcionam puramente por sorte. Eu sei bem disso — estremeço toda vez que tenho que dar manutenção ao meu código antigo baseado em Thread." — Primož Gabrijelčič, Delphi High Performance (p. 212)
2. A Verdade sobre múltiplos núcleos
A maioria dos dispositivos modernos — de smartphones a laptops e servidores — já conta com processadores multicore. No entanto, a grande maioria dos aplicativos ainda opera como se tivesse apenas um único núcleo disponível. Isso acontece porque, por padrão, aplicativos são desenvolvidos para executarem quase toda sua lógica na Main Thread.
Embora o sistema operacional (Windows, Android, iOS etc.) possa distribuir threads entre núcleos, isso só ocorre quando a aplicação cria explicitamente múltiplas threads. Se o código for sequencial e monothread, ele será limitado a um único núcleo — desperdiçando todo o potencial do processador.
"Multithreading allows you to increase the responsiveness of your application and, if your application runs on a multiprocessor or multi-core system, increase its throughput."
"O multithreading permite aumentar a responsividade da sua aplicação e, se ela for executada em um sistema com múltiplos processadores ou núcleos, incrementa o desempenho geral." — Microsoft Docs - Threads and Threading
"Most applications use just a single core and see no speed improvements when run on a multi-core machine. We need to write our programs in a new way."
"A maioria dos aplicativos utiliza apenas um núcleo, sem ganhos de desempenho em máquinas multicore. Precisamos repensar a forma como escrevemos nossos programas." — MSDN Magazine, 2007
3. O que é a TSafeThread4D?
Gerenciar Threads em Delphi, especialmente em aplicações móveis, envolve uma complexidade que vai além da simples execução paralela de tarefas. Em cenários comuns, como o consumo de serviços de backend, a aplicação precisa não apenas realizar a chamada remota, mas também coordenar cuidadosamente a experiência do usuário: desabilitar botões de ação para evitar múltiplos disparos, acionar indicadores visuais como TAniIndicator ou TProgressBar, e preparar a interface para o estado de "aguardando". Ao concluir a operação, é necessário reverter essas alterações e ainda tratar de forma consistente possíveis exceções. Esse encadeamento de responsabilidades, quando feito manualmente, tende a se tornar repetitivo, propenso a falhas e pouco sustentável em projetos de médio ou grande porte.
Além disso, no Android, há a pressão adicional do ANR (Application Not Responding), que pode comprometer toda a experiência caso a tarefa ultrapasse alguns segundos. Nesse contexto, a TSafeThread4D surge como uma abstração que organiza a execução assíncrona de forma segura e fluente, encapsulando o uso de Threads e callbacks em uma estrutura que garante previsibilidade, clareza e robustez ao fluxo entre backend e interface do usuário.
A TSafeThread4D disponibiliza um conjunto de callbacks que permitem ao desenvolvedor intervir em diferentes estágios do ciclo de execução da Thread, adaptando o comportamento da aplicação às suas necessidades específicas. Cada callback pode ser utilizado de forma independente, não sendo obrigatório implementar todos os pontos previstos pela classe. Essa flexibilidade garante que o desenvolvedor escolha apenas os trechos relevantes para o seu fluxo, mantendo o código mais limpo e expressivo. A seguir, detalhamos o papel de cada callback e como eles podem ser combinados para oferecer maior controle sobre a interação entre backend e interface do usuário.
4. Os Callbacks
Fluxo de Execução da TSafeThread4D
O diagrama abaixo ilustra, de forma simplificada, o ciclo de vida de uma thread criada com a TSafeThread4D, destacando os principais callbacks disponíveis e em qual contexto (UI Thread ou Worker Thread) eles são executados.
- OnInitialize (UI Thread)
Executado antes do início da thread em segundo plano. É o ponto ideal para configurar variáveis, preparar a interface ou inicializar recursos que precisam estar prontos antes da execução em paralelo. - OnExecute (Worker Thread)
Aqui é onde ocorre a lógica principal, rodando em paralelo sem bloquear a interface gráfica. Durante esse processo, é possível acionar o OnProgress, que retorna ao UI Thread para atualizar controles visuais (como progress bars ou logs). - Decisão do Resultado (Worker Thread → UI Thread)
Ao final da execução, a thread avalia o resultado:- OnSuccess (UI Thread): chamado se a execução ocorreu sem erros.
- OnError (UI Thread): acionado em caso de exceção.
- OnCancel (UI Thread): chamado se o cancelamento foi solicitado.
- OnTerminate (UI Thread)
Executado sempre no final, independentemente do resultado. É útil para liberar recursos, atualizar a interface e restaurar o estado da aplicação (como reabilitar botões ou esconder indicadores de progresso).

Observação Importante
O diagrama apresenta apenas os callbacks principais para simplificar a visualização do fluxo. Outros eventos também estão disponíveis na TSafeThread4D, mas foram omitidos aqui para manter o foco no ciclo básico de inicialização, execução, resultado e finalização.
WithOnInitialize — Preparação do cenário
- Descrição: Callback executado para preparar ou validar o ambiente antes da execução da tarefa principal (worker).
- Momento de disparo: imediatamente antes do
OnExecute. - Thread: UI thread (via
Synchronize). - Usos recomendados:Ler valores da interface (campos da UI).Validar parâmetros de entrada.Desabilitar botões ou controles interativos.Iniciar indicadores visuais (ex.:
TAniIndicator,TProgressbar). - Boas práticas:Restrinja a operações rápidas.Evite abertura de arquivos grandes ou consultas demoradas.
- Armadilhas comuns:Alterar estados críticos que impeçam o cancelamento antes do início do worker.
.WithOnInitialize(
procedure(Context: TThreadContext)
begin
LiveBindOff;
AniIndicatorInsertRecords.Visible := True;
AniIndicatorInsertRecords.Enabled := True;
pbarInsertRecords.Visible := True;
pbarInsertRecords.Value := 0;
if Sender is TButton then
TButton(Sender).Enabled := False;
MemoLog.Lines.Clear;
{$IFDEF ANDROID}
MemoLog.Lines.Add('[Initialize] Preparing 100,000 inserts...');
{$ELSE}
MemoLog.Lines.Add('[Initialize] Preparing 1,000,000 inserts...');
{$ENDIF}
end)
WithOnInitializeEvent — Integração com eventos existentes
- Descrição: Variante em estilo event-like do
OnInitialize, destinada a quem prefere delegar a execução a métodos de evento já declarados. - Momento de disparo / Thread: idêntico ao
OnInitialize— executado na UI thread (viaSynchronize) antes doOnExecute. - Usos recomendados:
- Reaproveitar handlers já definidos em formulários ou datamodules.
- Manter consistência em projetos que seguem padrão baseado em eventos.
- Boas práticas:
- Utilize quando a equipe ou o projeto já adota fortemente o modelo event-driven.
- Evite misturar chamadas de
WithOnInitializeeWithOnInitializeEventpara o mesmo fluxo, para não duplicar lógica.
- Armadilhas comuns:
- Acoplar demasiadamente a lógica ao formulário, reduzindo a reutilização em outras camadas.
procedure TForm1.InsertRecordsInialize(Sender: TObject);
begin
LiveBindOff;
AniIndicatorInsertRecords.Visible := True;
AniIndicatorInsertRecords.Enabled := True;
pbarInsertRecords.Visible := True;
pbarInsertRecords.Value := 0;
btnInsertRecords.Enabled := False;
MemoLog.Lines.Clear;
{$IFDEF ANDROID}
MemoLog.Lines.Add('[Initialize] Preparing 100,000 inserts...');
{$ELSE}
MemoLog.Lines.Add('[Initialize] Preparing 1,000,000 inserts...');
{$ENDIF}
end;
...
.WithOnInitializeEvent(InsertRecordsInialize)WithOnExecute — Execução do trabalho pesado
- Descrição: Callback principal responsável pela lógica de processamento em segundo plano.
- Momento de disparo: após o
OnInitialize. - Thread: Worker thread.
- Usos recomendados:
- Operações de I/O (arquivos, rede, banco de dados).
- Processamento intensivo de CPU (CPU-bound tasks).
- Transformações de dados e manipulação de coleções.
- Cópia de arquivos ou chamadas HTTP/REST.
- Boas práticas:
- Utilize
CheckCancel/CheckTimeoutem pontos estratégicos para permitir cancelamento limpo. - Relate progresso via
ReportProgress. - Nunca interaja diretamente com a UI — utilize os callbacks apropriados.
- Utilize
- Armadilhas comuns:
- Loops excessivamente "quentes" (tight loops), introduza cadência (ex.: if (i and $FF) = 0 then ...) para reduzir overhead.
- Acúmulo de exceções não tratadas pode encerrar a thread abruptamente.
.WithOnExecute(
procedure(Context: TThreadContext)
var
I, TotalRecords, Step: Integer;
// Field caching (avoid FieldByName in the loop)
fldID, fldName, fldBirth, fldActive, fldBalance, fldNotes,
fldCreated:TField;
HadIndexes: Boolean;
Params: ISafeThread4DParams; // scoped strong ref
begin
// Weak -> Strong
Params := ISafeThread4DParams(IInterface(ParamsRaw));
if Params = nil then
raise Exception.Create('Internal error: ParamsRaw
not initialized.');
{$IFDEF ANDROID}
TotalRecords := 100_000;
{$ELSE}
TotalRecords := 1_000_000;
{$ENDIF}
Step := TotalRecords div 10;
if Step <= 0 then Step := 1;
// Initial cooperative check
TSafeThread4D.CheckCancel(Params, Context);
// If using timeout: TSafeThread4D.CheckTimeout(Params, Context);
// Field cache
fldID := FDMemTable.FindField('ID');
fldName := FDMemTable.FindField('Name');
fldBirth := FDMemTable.FindField('BirthDate');
fldActive := FDMemTable.FindField('IsActive');
fldBalance := FDMemTable.FindField('Balance');
fldNotes := FDMemTable.FindField('Notes');
fldCreated := FDMemTable.FindField('Created');
if (fldID = nil) or (fldName = nil) then
raise Exception.Create('Missing required fields: "ID" and/or
"Name"');
// Batch optimizations
HadIndexes := FDMemTable.IndexesActive;
if HadIndexes then
FDMemTable.IndexesActive := False;
FDMemTable.DisableControls;
try
FDMemTable.EmptyDataSet;
for I := 1 to TotalRecords do
begin
// Cheap cooperative check every 1024 iterations.
// (I and $3FF) = 0 <-> I mod 1024 = 0. Bitwise is faster than
modulo.
if (I and $3FF) = 0 then
begin
TSafeThread4D.CheckCancel(Params, Context);
// Also call CheckTimeout here if you enable a timeout.
// TSafeThread4D.CheckTimeout(Params, Context);
end;
FDMemTable.Append;
try
fldID.AsGuid := TGUID.NewGuid;
fldName.AsString := 'Taro ' + IntToStr(I);
if Assigned(fldBirth) then
fldBirth.AsDateTime := EncodeDate(2024, 1, 1) + I;
if Assigned(fldActive) then
fldActive.AsBoolean := (I mod 2 = 0);
if Assigned(fldBalance) then
fldBalance.AsCurrency := Random * 1000;
if Assigned(fldNotes) then
fldNotes.AsString := 'This is a memo field for record ' +
IntToStr(I);
if Assigned(fldCreated) then
fldCreated.AsDateTime := Now - (I mod 365);
FDMemTable.Post;
except
on E: Exception do
begin
FDMemTable.Cancel;
raise;
end;
end;
// Throttled progress (checkpoints and tail)
if ((I mod Step) = 0) or (I = TotalRecords) then
TSafeThread4D.ReportProgress(Params, I / TotalRecords);
end;
// Ensure 100% if the last checkpoint missed
if (TotalRecords mod Step) <> 0 then
TSafeThread4D.ReportProgress(Params, 1.0);
finally
FDMemTable.EnableControls;
if HadIndexes then
FDMemTable.IndexesActive := True;
FDMemTable.Last;
end;
end);WithOnSuccess — Finalização bem-sucedida
- Descrição: Callback acionado quando a execução da tarefa é concluída com êxito, sem erro, cancelamento ou timeout.
- Momento de disparo: após o término do
OnExecute, somente em caso de sucesso. - Thread: UI thread (via
Synchronize). - Usos recomendados:
- Atualizar a interface com os resultados obtidos.
- Reabilitar botões e controles desativados durante a execução.
- Exibir mensagens de confirmação ou indicadores de "Concluído".
- Boas práticas:
- Centralize aqui apenas a lógica de pós-processamento ligada à experiência do usuário.
- Evite repetir validações ou reprocessar dados — esta etapa deve ser rápida.
- Armadilhas comuns:
- Não presumir que este callback sempre será executado: em caso de erro, cancelamento ou timeout, ele não será disparado.
.WithOnSuccess(
procedure(Context: TThreadContext)
begin
pbarInsertRecords.Value := 100; // Ensure visual 100%
MemoLog.Lines.Add('[Success] Insertion completed');
end)WithOnComplete — Encerramento complementar
- Descrição: Callback opcional de fechamento, executado apenas quando explicitamente habilitado (
LDoComplete = True). - Momento de disparo: após o
OnSuccess(fluxo de sucesso), nunca em cenários de erro, cancelamento ou timeout. - Thread: UI thread (via
Synchronize). - Usos recomendados:
- Execução de rotinas de cleanup adicionais.
- Flush de telemetria ou métricas de execução.
- Persistência final de estados ou resultados no backend.
- Boas práticas:
- Use para atividades complementares, não essenciais ao fluxo principal.
- Mantenha a lógica leve, evitando operações demoradas.
- Armadilhas comuns:
- Não substitui o
OnTerminate, que pertence ao ciclo de vida da thread. - Não é chamado em fluxos de erro, cancelamento ou timeout.
- Não substitui o
CRIARWithOnTerminate — Finalização garantida do ciclo
- Descrição: Callback de término do ciclo da thread, executado de forma garantida, independentemente do resultado da execução.
- Momento de disparo: sempre ao final da thread — seja em caso de sucesso, erro, cancelamento ou timeout.
- Thread: UI thread (via
Synchronize). - Usos recomendados:
- Reabilitar controles da UI que foram desativados durante a execução.
- Liberar handles externos ou recursos que dependem do contexto da UI.
- Boas práticas:
- Utilize para ações de restauração e liberação de recursos.
- Mantenha o código enxuto, evitando sobrecarregar a UI nesse ponto.
- Armadilhas comuns:
- Não confundir com
OnComplete(executado apenas em fluxos de sucesso quando habilitado). - Não confundir com
OnSuccess, que só ocorre se não houver falhas, cancelamento ou timeout.
- Não confundir com
.WithOnTerminate(
procedure(Context: TThreadContext)
begin
LiveBindOn;
AniIndicatorInsertRecords.Enabled := False;
AniIndicatorInsertRecords.Visible := False;
if Sender is TButton then
TButton(Sender).Enabled := True;
MemoLog.Lines.Add(
Format('[Terminate] Elapsed Time: %.3f s',
[Context.ElapsedMilliseconds / 1000]));
// Release the reference so next clicks can start new inserts
FInsertRecordsParams := nil;
end)
WithOnTerminateEvent — Compatibilidade com Thread.OnTerminate
- Descrição: Variante em estilo event-like que reproduz a semântica clássica do
TThread.OnTerminate, permitindo integração com código legado. - Momento de disparo: sempre no final da thread, independentemente do resultado.
- Thread: UI thread (via
Synchronize). - Usos recomendados:
- Manter compatibilidade com projetos existentes que já utilizam
TThread.OnTerminate. - Facilitar migrações graduais de código legado para a
TSafeThread4D.
- Manter compatibilidade com projetos existentes que já utilizam
- Boas práticas:
- Utilize preferencialmente
WithOnTerminateem código novo, reservando esta variante apenas para cenários de compatibilidade.
- Utilize preferencialmente
- Armadilhas comuns:
- Pode induzir a manter acoplamento desnecessário com padrões legados.
private
FElapsedMs: Double;
...
procedure TForm1.InsertRecordsTerminate(Sender: TObject);
// Used by .WithOnTerminateEvent(InsertRecordsTerminate)
begin
LiveBindOn;
AniIndicatorInsertRecords.Enabled := False;
AniIndicatorInsertRecords.Visible := False;
btnInsertRecords.Enabled := True;
MemoLog.Lines.Add(Format('[Terminate] Elapsed Time: %.3f s',
[FElapsedMs / 1000]));
FInsertRecordsParams := nil;
end;
...
.WithOnTerminate(
procedure(Context: TThreadContext)
begin
FElapsedMs := Context.ElapsedMilliseconds;
end)
.WithOnTerminateEvent(InsertRecordsTerminate)WithOnError — Tratamento de falhas sem travar a UI
- Descrição: Callback acionado quando uma exceção é levantada durante o
OnExecutee não se trata de cancelamento ou timeout. - Momento de disparo: imediatamente após a captura da exceção no fluxo de trabalho.
- Thread: UI thread via
TThread.Queue(não bloqueante). - Usos recomendados:
- Exibir mensagens ao usuário (evitar métodos bloqueantes como dialogs).
- Registrar logs e enviar telemetria/diagnósticos.
- Boas práticas:
- Mantenha a lógica leve; delegue análises pesadas para uma worker separada.
- Normalize exceções (mapeie para códigos/erros de domínio) antes de apresentar à UI.
- Garanta idempotência das ações de erro (evite múltiplas mensagens para a mesma falha).
- Armadilhas comuns:
- Como usa
Queue, a ordem pode se intercalar com outros posts para a UI; não presuma execução estritamente sequencial com eventos paralelos. - Evite operações potencialmente bloqueantes na UI (ex.: diálogos modais longos) logo após a falha.
- Não trate aqui cancelamento ou timeout — esses caminhos têm callbacks e semânticas próprias.
- Como usa
.WithOnError(
procedure(const ErrorMessage: string; const Context: TThreadContext)
begin
MemoLog.Lines.Add('[Error] ' + ErrorMessage);
end)WithOnCancel — Encerramento por cancelamento do usuário
- Descrição: Callback acionado quando a operação é interrompida de forma cooperativa via
CheckCancel, resultando emEOperationCancelled. - Momento de disparo: após o cancelamento ser detectado no fluxo da worker.
- Thread: UI thread (via
Synchronize). - Usos recomendados:
- Atualizar a UI para refletir o estado "interrompido".
- Executar rollback de resultados parciais ou limpezas intermediárias.
- Reabilitar ações de entrada (botões, menus) permitindo nova tentativa.
- Boas práticas:
- Informe o usuário de maneira clara que a tarefa foi cancelada.
- Libere recursos intermediários de forma previsível, como buffers ou conexões abertas.
- Armadilhas comuns:
- Só será disparado em cancelamento cooperativo — o worker deve invocar
CheckCancelem pontos apropriados. - Cancelamentos forçados (ex.: kill thread) não acionam este callback.
- Só será disparado em cancelamento cooperativo — o worker deve invocar

.WithOnCancel(
procedure(Context: TThreadContext)
begin
MemoLog.Lines.Add('[Cancel] Insert operation canceled by user');
endWithOnProgress — Atualizações de progresso sem bloquear (com throttling)
- Descrição: Callback de progresso que recebe valores normalizados em [0..1] durante o
OnExecute. - Momento de disparo: ao longo da execução do worker.
- Thread: UI thread via
TThread.Queue(não bloqueante). - Usos recomendados:
- Atualizar
TProgressBar, labels com % concluído e itens processados. - Exibir throughput (itens/seg) e tempo restante estimado.
- Pequenos pings de vivacidade ("ainda trabalhando…").
- Atualizar
Por que usar throttle
- Proteção do frame budget da UI: telas a 60 Hz têm ~16 ms por quadro; despejar dezenas/centenas de posts por segundo para a UI causa jank, stutter e queda de FPS.
- Evitar "tempestade" de mensagens:
Queueenfileira; sem throttle o loop de mensagens infla, aumenta latência e consumo de memória, e pode intercalar eventos de forma indesejada. - Reduzir custo de layout/paint: cada atualização de barra/label pode disparar layout e repaint; coalescar atualizações economiza CPU e GPU.
- Menos risco de ANR no Android: UI ocupada reagindo a progresso "chuvoso" + GC + binder calls = janela mais estreita para entrada do usuário.
- Bateria e aquecimento: menos wakeups e invalidations, menos aquecimento, melhor autonomia.
- Sem perda semântica: progresso é monotônico; o usuário não precisa ver cada 0,1% — ver tendência suave e responsiva é melhor que "contagem de grãos de areia".
Boas práticas:
- Defina uma janela de throttle adequada: use intervalos curtos (50–150 ms) para manter fluidez em UIs comuns e maiores (200–500 ms) em telas pesadas ou com muitos elementos visuais.
- Coalescamento last - write - wins: durante a janela, mantenha apenas o último valor de progresso e entregue-o no próximo tick, evitando updates redundantes.
- Sempre faça flush final: garanta que o valor 1.0 (100%) seja emitido, mesmo que a janela de throttle ainda não tenha expirado.
- Prefira relatórios compostos: em vez de múltiplos callbacks fragmentados, agregue no mesmo handler as métricas de progresso (% concluído, itens processados, throughput, ETA (Estimated Time of Arrival).
- Use suavização opcional: para métricas como throughput ou ETA, utilize médias móveis, ex.: EWMA (Exponentially Weighted Moving Average) no worker e envie valores suavizados para a UI.
- Escolha granularidade adequada no worker: invoque
ReportProgressem pontos naturais (fim de lote, a cada N itens ou após certo tempo), e nunca dentro de tight loops por item. - Armadilhas comuns:
- "Perda" de porcentagens intermediárias: é intencional; não dependa de receber "cada 1%".
- Supor ordem estrita com outros eventos de UI: como usa
Queue, eventos podem se intercalar; projete a UI para ser idempotente e tolerante a ordem. - Atualizações pesadas na UI: mantenha o handler de progresso leve (sem IO, sem cálculos grandes).
- Granularidade exagerada no worker: chamar
ReportProgressem cada item degrada a UI; prefira lote/tempo. - Esquecer do flush final: pode deixar a barra "travada" em 99% se a janela de throttle não disparar novamente.
.WithOnProgress(
procedure(Pct: Single)
begin
pbarInsertRecords.Value := Pct * 100;
lblPercentageInsertRecords.Text := Format('%.0f%%', [Pct * 100]);
end)
WithProgressIntervalMs — Controle de throttle das atualizações de progresso
- Descrição: Define o intervalo mínimo (em milissegundos) entre posts consecutivos de progresso para a UI.
- Valor padrão: 100 ms.
- Efeito: o primeiro
ReportProgressé entregue imediatamente; chamadas subsequentes são coalescidas e só liberadas quando o intervalo configurado expira. - Usos recomendados:
- Reduzir carga da UI: impedir que a message queue seja inundada em operações que disparam progresso com alta frequência.
- UIs de alta responsividade: diminuir o valor (ex.: 50 ms) em aplicações que exigem sensação quase realtime (ex.: streaming, animações, gráficos dinâmicos).
- Operações muito verbosas: aumentar o valor (ex.: 200–500 ms) para cenários em que há milhares de itens processados, mas o usuário só precisa ter uma noção macro da evolução.
- Boas práticas:
- Ajuste de acordo com o tipo de UI: menor em dashboards "vivos", maior em ETLs (Extract, Transform, Load) ou cargas pesadas de dados.
- Combine com
ReportProgressem pontos naturais (lotes, checkpoints de tempo), não a cada item processado. - Sempre mantenha flush final em 1.0 (100%), independente do intervalo.
- Armadilhas comuns:
- Valores muito baixos podem causar jank ou travar a experiência em dispositivos móveis.
- Valores muito altos podem transmitir sensação de "barra parada" ou congelada.
- Não confundir: este parâmetro regula apenas a cadência de entrega à UI, não a frequência de chamadas a
ReportProgressno worker.
.WithProgressIntervalMs(50)WithTimeoutMs — Limite de tempo (timeout cooperativo)
- Descrição: Define o tempo máximo permitido para a execução da tarefa em worker thread.
- Funcionamento: durante o processamento, chamadas explícitas a
Ctx.CheckTimeoutverificam o tempo decorrido desde o início da execução. Caso(NowTick - StartTick) >= TimeoutMs, é levantada a exceçãoEOperationTimeout. - Usos recomendados:
- Impedir que operações de rede, I/O ou CPU-bound fiquem presas indefinidamente.
- Garantir responsividade em dispositivos móveis, reduzindo risco de ANR no Android.
- Implementar limites contratuais (ex.: cada requisição deve encerrar em até 5 segundos).
- Boas práticas:
- Sempre combine com
CheckCancelpara permitir cancelamento explícito e timeout automático no mesmo fluxo. - Insira
CheckTimeoutem pontos naturais do worker (fim de lote, página, bloco de cálculo), em vez de dentro de cada iteração. - Ao capturar
EOperationTimeout, trate-o noWithOnErrorou em handlers específicos, comunicando claramente ao usuário que houve expiração.
- Sempre combine com
- Armadilhas comuns:
- Sem chamadas a
CheckTimeout, o timeout nunca será observado, lembre-se de posicioná-lo dentro do worker. - Usar intervalos excessivamente curtos pode abortar tarefas que ainda estão em progresso legítimo.
- Misturar
Sleeplongos no worker atrapalha a observação do timeout.
- Sem chamadas a
.WithTimeoutMs(3000)
WithOnTimeout — Tratamento de estouro de tempo
- Descrição: Callback acionado quando a execução excede o limite configurado e é levantada
EOperationTimeout. - Momento de disparo: após a captura da exceção de timeout no fluxo da worker.
- Thread: UI thread (via
Synchronize). - Usos recomendados:
- Informar claramente o usuário sobre a expiração (mensagem orientando próxima ação).
- Registrar telemetria de latência e timeouts para observabilidade — SLO (Service Level Objective) / SLA (Service Level Agreement).
- Sugerir ajuste de parâmetros operacionais (reduzir tamanho de lote, aumentar timeout, tentar novamente).
- Habilitar caminhos de recuperação (botão "Tentar de novo", alternar endpoint, fallback de cache).
- Boas práticas:
- Diferencie timeout de outras falhas no
WithOnError(mensagens e métricas específicas). - Considere retry com backoff exponencial e jitter — nunca retry imediato em laço apertado.
- Se a operação for idempotente, explicite isso na UI para permitir nova tentativa segura.
- Reavalie limites: timeout muito curto em redes móveis pode ser irrealista; ajuste de acordo com condições reais.
- Diferencie timeout de outras falhas no
- Armadilhas comuns:
- Como o término ocorreu por exceção, revise a rotina de cleanup para evitar resíduos (arquivos temporários, handles, transações abertas).
- Não trate timeout como "cancelamento do usuário" — fluxos e mensagens são distintos.
- Evite bloquear a UI com diálogos modais longos ao notificar o timeout.
- Não oculte a causa: reporte métricas de latência e timeouts; são sinais de saturação ou tail latency.
.WithOnTimeout(
procedure(Context: TThreadContext)
begin
MemoLog.Lines.Add('[Timeout] Operation timed out');
end)
WithHeartbeatIntervalMs / WithHeartbeat — Sinal de vivacidade (heartbeat)
- Descrição: Mecanismo leve de watchdog que envia pings periódicos à UI (via
Queue(...)) para indicar que a aplicação segue responsiva, mesmo durante operações longas. - Funcionamento: uma thread auxiliar que posta mensagens a cada N ms, interrompendo automaticamente quando o worker conclui.
- Objetivo principal: prevenir que o Android interprete a aplicação como travada (ANR), mantendo a janela ativa e responsiva.
- Usos recomendados:
- Tarefas longas em Android (ex.: operações de rede extensas, processamento em lote).
- Telas críticas que exibem indicadores de atividade contínua (spinners, animações, barras de status).
- Situações em que há risco de a UI "parecer congelada" enquanto aguarda resultados.
- Boas práticas:
- Use apenas em operações realmente demoradas; para tarefas curtas, não traz benefício.
- Combine com
WithOnProgresspara mostrar progresso real sempre que possível, usando heartbeat apenas como "sinal de vida" extra. - Mantenha o intervalo razoável (ex.: 500–1000 ms); valores muito baixos aumentam carga desnecessária.
- Lembre-se: heartbeat não é cura para UI bloqueada — o trabalho pesado deve sempre estar no worker thread.
- Armadilhas comuns:
- Supor que o heartbeat resolve travamentos de UI: se o worker tocar diretamente na UI, ANRs continuarão a ocorrer.
- Intervalo muito agressivo pode gerar tráfego excessivo na message queue.
- Não confundir "pings" de heartbeat com progresso real: são sinais de vivacidade, não indicadores de avanço percentual.
.WithHeartbeatIntervalMs(300)
.WithOnHeartbeat(
procedure
var
blip: Char;
ts: string;
begin
// No extra units needed: blink via tick count
if Odd(TThread.GetTickCount64 div 300) then blip := '●' else blip
:= '◦';
ts := FormatDateTime('hh:nn:ss', Now);
{$IFDEF ANDROID}
lblInsertRecordsStatus.Text := Format('[HB] ANR guard %s %s',
[blip, ts]);
{$ELSE}
lblInsertRecordsStatus.Text := Format('[HB] UI ping %s %s', [blip,
ts]);
{$ENDIF}
end)WithFreeOnTerminate — Liberação automática do objeto
- Descrição: Espelha o comportamento do
TThread.FreeOnTerminate, liberando automaticamente a instância da thread wrapper quando o ciclo é concluído. - Quando usar:
- Cenários de fire-and-forget, em que não há necessidade de inspecionar resultados ou estados após a execução.
- Quando você mantém apenas weak handles (referências fracas) para a thread e não precisa coordenar sua liberação manualmente.
- Fluxos auxiliares de curta duração em que a sobrevida da thread não afeta recursos críticos.
- Boas práticas:
- Documente claramente o uso de liberação automática.
- Prefira usá-lo em cenários simples, onde a thread não precisa expor métricas, logs ou resultados ao chamador.
- Armadilhas comuns:
- Se houver necessidade de inspecionar resultados, exceções ou estado final da tarefa, não utilize liberação automática — nesse caso, mantenha o objeto vivo até concluir a inspeção.
- Referências "penduradas" (dangling references) podem ocorrer se outro trecho do código assumir que a instância ainda está disponível após o término.
- Em fluxos complexos ou críticos, a liberação explícita costuma ser mais segura e previsível.
.WithFreeOnTerminate(True)WithCompleteWithError — "Complete" mesmo em falha
- Descrição: Habilita o hook de
OnCompletetambém no caminho de erro (falha noOnExecute), sem substituir oWithOnError. - Momento de disparo: no fluxo de falha, após o tratamento primário de erro; no fluxo de sucesso, segue igual ao
OnComplete. - Thread: UI thread (via
Synchronize, tal comoOnComplete). - Usos recomendados:
- Garantir cleanup homogêneo de UI e recursos (fechar sessão visual, encerrar indicadores) mesmo quando houve erro.
- Padronizar encerramento de tela/estado, mantendo o
WithOnErrorpara feedback e telemetria.
- Boas práticas:
- Mantenha o handler idempotente (executável tanto após sucesso quanto após erro, sem efeitos duplicados).
- Separe responsabilidades: mensagens/telemetria no
WithOnError; restauração de estado/cleanup aqui. - Registre no handler o motivo do término (sucesso vs. erro) caso precise ajustar pequenos detalhes de UI.
- Armadilhas comuns:
- Não confunda com
WithOnTerminate: este já é garantido para qualquer resultado e continua sendo o ponto derradeiro do ciclo. - Risco de dupla liberação se parte do cleanup também ocorre em
OnTerminate— desenhe os dois handlers para não colidirem. - Aplica-se a erro; caminhos de cancelamento e timeout têm callbacks próprios (
WithOnCancel,WithOnTimeout) e geralmente não devem acionarOnCompletepor consistência semântica.
- Não confunda com
.WithCompleteWithError(True)
WithThreadPriority — Ajuste de prioridade da thread (somente Windows)
- Descrição: Define a prioridade do worker thread, mapeando diretamente para
TThread.Priorityno Windows. - Quando usar:
- Baixa prioridade: tarefas de fundo, longas ou não críticas, evitando competição direta com a UI ou com operações sensíveis.
- Alta prioridade: operações curtas, críticas e que precisam de resposta imediata (ex.: cálculos rápidos necessários antes de liberar a UI).
- Boas práticas:
- Use prioridade baixa para fluxos de manutenção, pré-cálculos ou logging.
- Restrinja prioridade alta a operações pontuais; mantenha a maior parte do trabalho em prioridade normal.
- Sempre meça impacto em cenários reais antes de adotar prioridade não padrão.
- Armadilhas comuns:
- Configurar prioridade alta constante pode degradar a responsividade global da aplicação, inclusive travando animações e input lag.
- A alteração é plataforma-dependente: em Android/iOS/macOS o ajuste é ignorado.
- Não confundir prioridade de thread com timeout ou cancelamento — são mecanismos diferentes.
{$IFDEF MSWINDOWS}
.WithThreadPriority(PriorityHigher)
{$ENDIF}WithThreadName — Nome amigável para debugging
- Descrição: Define um nome legível para a worker thread, tornando-o visível em depuradores compatíveis (via
NameThreadForDebugging). - Usos recomendados:
- Depuração: identificar rapidamente qual thread corresponde a determinada tarefa.
- Profiling: facilitar análise de desempenho em ferramentas que exibem múltiplas threads.
- Logging: registrar o nome da thread em logs ou telemetria para rastreabilidade.
- Boas práticas:
- Escolha nomes curtos, descritivos e consistentes com o domínio da aplicação (ex.:
"SyncOrders","ImageResize","TelemetryFlush").
- Escolha nomes curtos, descritivos e consistentes com o domínio da aplicação (ex.:
- Prefira nomes estáticos ou padronizados — nomes dinâmicos em excesso podem atrapalhar leitura em depuradores.
- Armadilhas comuns:
- Disponibilidade limitada: a visibilidade do nome depende do suporte do OS/depurador (mais confiável em Windows, suporte variável em outras plataformas).
- Não confundir com identificadores lógicos do aplicativo: é apenas metadado para debugging/profiling, sem efeito funcional.
- Alterar o nome não interfere em agendamento ou prioridade da thread.
.WithThreadName('Insert-Records')
WithThreadId — Exposição do identificador nativo da thread
- Descrição: Disponibiliza de forma conveniente o ID nativo da worker thread, permitindo sua captura e uso em pontos do ciclo de vida.
- Usos recomendados:
- Correlação de logs: registrar o ID da thread em mensagens de log para rastrear fluxos concorrentes.
- Suporte técnico: fornecer informações detalhadas em relatórios de erro ou dumps de execução.
- Profiling/Tracing: integrar o ID nativo em ferramentas de análise de desempenho ou telemetria.
- Boas práticas:
- Use o ID como complemento de diagnósticos, sempre junto de contexto semântico (nome da tarefa,
WithThreadName). - Armazene IDs apenas para fins temporários de rastreamento — eles podem variar a cada execução.
- Em cenários multi-thread complexos, combine ID + timestamp para obter rastreabilidade mais clara.
- Use o ID como complemento de diagnósticos, sempre junto de contexto semântico (nome da tarefa,
- Armadilhas comuns:
- O ID nativo é dependente da plataforma e não deve ser usado como chave lógica no domínio da aplicação.
- IDs podem ser reutilizados pelo OS após o término da thread, não persista indefinidamente.
- Não confundir com
TThread.CurrentThread.ThreadID(Delphi), que retorna o ID gerenciado; aqui trata-se do identificador do sistema operacional.
...
P :=
TSafeThread4DParams.New
.WithThreadName('Download JSON')
.WithThreadId(3)
...
.WithOnInitialize(
procedure(Context: TThreadContext)
begin
AniIndicatorJSON.Visible := True;
AniIndicatorJSON.Enabled := True;
if Sender is TButton then
TButton(Sender).Enabled := False;
MemoDownloadLog.Lines.Clear;
MemoDownloadLog.Lines.Add(Format(
'[Init] Thread "%s" (Native ID: %d - Logical ID: %d) has
started.', [Context.ThreadName, Context.NativeThreadID,
Context.LogicalThreadID]));
end)WithMeasureTime — Cronometragem automática da execução
- Descrição: Ativa a medição de tempo de execução da thread, preenchendo
Context.ElapsedMillisecondsao término da operação. - Usos recomendados:
- Telemetria: coletar tempos de resposta de operações críticas.
- Métricas de UX: avaliar se a experiência do usuário está dentro de limites aceitáveis.
- Comparação de abordagens: medir impacto de diferentes algoritmos ou estratégias (benchmark leve).
- Boas práticas:
- Use em conjunto com
WithOnSuccess,WithOnErroreWithOnTimeoutpara registrar tempos por cenário.
- Use em conjunto com
- Utilize os dados para ajustar timeouts realistas e expectativas de desempenho.
- Armadilhas comuns:
- Não substitui profilers completos (ex.: sampling/tracing detalhado), mas é extremamente útil para métricas operacionais e comparações práticas.
- Pode introduzir overhead mínimo em operações extremamente curtas (geralmente irrelevante na prática).
- Medição é feita no nível do ciclo da thread — não fornece granularidade interna de cada etapa.
.WithMeasureTime(True)
.WithOnTerminate(
procedure(Context: TThreadContext)
begin
AniIndicatorJSON.Enabled := False;
AniIndicatorJSON.Visible := False;
if Sender is TButton then
TButton(Sender).Enabled := True;
MemoDownloadLog.Lines.Add(Format(
'[Terminate] Thread "%s" (Native ID: %d - Logical ID: %d)
completed. Elapsed time: %.3f seconds.',
[Context.ThreadName, Context.NativeThreadID,
Context.LogicalThreadID, Context.ElapsedMilliseconds / 1000]));
// Deterministic release + allow re-entry
LResult.Free;
FDownloadJSONParams := nil;
end);
5. Dicas gerais que valem para todos os callbacks
- UI só na UI: use os callbacks de UI; jamais toque em controles dentro do worker.
- Cooperatividade: coloque
CheckCancel/CheckTimeoutonde faz sentido (bordas de lote, a cada N iterações, fim de etapa). - Progresso realista: throttle protege sua UI; prefira menos posts de progresso com marcos significativos.
- Fluxo mental: Execute faz; Success/Complete comemoram/fecham; Error/Cancel/Timeout informam e recuperam; Terminate garante o reset final.
6. TSafeThread4D — Tabela-Resumo de Callbacks
| Callback / Config | Thread | Quando dispara | Uso típico |
|---|---|---|---|
| WithOnInitialize | UI (Synchronize) | Antes do OnExecute | Validar UI, preparar ambiente |
| WithOnInitializeEvent | UI | Idem ao acima | Reusar métodos/eventos existentes |
| WithOnExecute | Worker | Durante execução | Trabalho pesado (I/O, CPU, rede) |
| WithOnSuccess | UI (Synchronize) | Após execução sem erros/cancel/timeout | Atualizar UI com resultado |
| WithOnComplete | UI (Synchronize) | Após sucesso (se LDoComplete=True) | Fechamento opcional, salvar estado |
| WithOnTerminate | UI (Synchronize) | Sempre no fim (qualquer resultado) | Reabilitar UI, limpeza final |
| WithOnTerminateEvent | UI | Compatível com TThread.OnTerminate | Integração com código legado |
| WithOnError | UI (Queue) | Quando ocorre exceção no OnExecute | Logar e exibir mensagens de erro |
| WithOnCancel | UI (Synchronize) | Quando usuário cancelou (CheckCancel) | Avisar cancelamento, rollback parcial |
| WithOnProgress | UI (Queue) | Durante OnExecute (com throttle) | Atualizar barras de progresso |
| WithProgressIntervalMs | — | Define intervalo mínimo entre ReportProgress | Evitar excesso de mensagens |
| WithTimeoutMs | — | Define tempo máximo para execução | Cancelar tarefas que demoram demais |
| WithOnTimeout | UI (Synchronize) | Quando CheckTimeout expira | Avisar usuário sobre tempo esgotado |
| WithHeartbeatIntervalMs | — | Ping periódico p/ manter UI viva | Evitar ANR no Android |
| WithHeartbeat | — | Ativa/desativa heartbeat | Alternativa simplificada |
| WithFreeOnTerminate | — | Liberação automática da thread | Fire-and-forget |
| WithCompleteWithError | — | Permite rodar OnComplete mesmo em erro | Cleanup homogêneo |
| WithThreadPriority | — | Define prioridade da thread | Balancear responsividade x velocidade |
| WithThreadName | — | Nomeia thread p/ debugging | Logs, profiling, diagnóstico |
| WithThreadId | — | Expõe ID nativo da thread | Correlacionar logs |
| WithMeasureTime | — | Mede tempo de execução (ElapsedMilliseconds) | Telemetria, métricas, comparações |
7. Exemplos no GitHub
Os exemplos completos de utilização da TSafeThread4D estão disponíveis no repositório do projeto no GitHub. No formulário de demonstração, você encontrará:
- Um
TControlprincipal com várias abas, organizando diferentes cenários de uso. - Diversos botões de teste, cada um disparando operações distintas via
TSafeThread4D. - A possibilidade de acionar todos os botões simultaneamente, observando que o aplicativo permanece fluido e responsivo, sem travamentos na UI.
Essa demonstração foi construída justamente para evidenciar que, mesmo em situações de concorrência intensa, o modelo de callbacks e o gerenciamento seguro de threads mantêm a experiência do usuário estável.
Conclusão
Controlar threads nunca foi tarefa simples. Entre Synchronize, Queue, ANR, UI freezes e callbacks que se espalham pelo código, não é difícil acabar com uma aplicação instável e um desenvolvedor cansado. A TSafeThread4D nasceu para atacar exatamente esse ponto: dar previsibilidade e clareza ao ciclo de vida assíncrono no Delphi.
Nos exemplos disponíveis no GitHub, você pode disparar todas as operações ao mesmo tempo — e verá que a aplicação não trava. A UI continua lá, viva, receptiva, como deveria ser desde o início.
Não há mágica. O que existe é disciplina: callbacks bem definidos, throttle para domar eventos ruidosos, tratamento explícito de erro, timeouts cooperativos e finalização garantida.
No fundo, a TSafeThread4D não é só mais uma classe. É um convite a escrever código mais transparente: onde o que acontece em segundo plano não ameaça o que acontece diante dos olhos do usuário.