TheCodeNaked

TSafeThread4D — Execução Segura e Produtiva de Threads no Delphi (Copy)

A TSafeThread4D não é apenas uma abstração para facilitar o uso de threads. Ela representa uma forma moderna de programar no Delphi, aproveitando o poder dos múltiplos núcleos e mantendo as aplicações responsivas, seguras e fáceis de manter.

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 Threadscallbacks em uma estrutura que garante previsibilidade, clareza e robustez ao fluxo entre backend e interface do usuário.

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.

  1. 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.
  2. 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).
  3. Decisão do Resultado (Worker ThreadUI 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.
  4. 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 (via Synchronize) antes do OnExecute.
  • 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 WithOnInitialize e WithOnInitializeEvent para 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 / CheckTimeout em pontos estratégicos para permitir cancelamento limpo.
    • Relate progresso via ReportProgress.
    • Nunca interaja diretamente com a UI — utilize os callbacks apropriados.
  • 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.
CRIAR

WithOnTerminate — 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.
.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.
  • Boas práticas:
    • Utilize preferencialmente WithOnTerminate em código novo, reservando esta variante apenas para cenários de compatibilidade.
  • 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 OnExecute e 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.
.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 em EOperationCancelled.
  • 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 CheckCancel em pontos apropriados.
    • Cancelamentos forçados (ex.: kill thread) não acionam este callback.

.WithOnCancel(
  procedure(Context: TThreadContext)
  begin
    MemoLog.Lines.Add('[Cancel] Insert operation canceled by user');
  end

WithOnProgress — 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 TProgressBarlabels com % concluído e itens processados.
    • Exibir throughput (itens/seg) e tempo restante estimado.
    • Pequenos pings de vivacidade ("ainda trabalhando…").

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 jankstutter e queda de FPS.
  • Evitar "tempestade" de mensagens: Queue enfileira; 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 ReportProgress em 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 ReportProgress em 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 ReportProgress em 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 ReportProgress no 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.CheckTimeout verificam o tempo decorrido desde o início da execução. Caso (NowTick - StartTick) >= TimeoutMs, é levantada a exceção EOperationTimeout.
  • 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 CheckCancel para permitir cancelamento explícito e timeout automático no mesmo fluxo.
    • Insira CheckTimeout em 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 no WithOnError ou em handlers específicos, comunicando claramente ao usuário que houve expiração.
  • 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 Sleep longos no worker atrapalha a observação do timeout.
.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.
  • 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 WithOnProgress para 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 resultadosexceçõ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 OnComplete também no caminho de erro (falha no OnExecute), sem substituir o WithOnError.
  • 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 como OnComplete).
  • 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 WithOnError para 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 WithOnErrorrestauraçã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 (WithOnCancelWithOnTimeout) e geralmente não devem acionar OnComplete por consistência semântica.
.WithCompleteWithError(True)

WithThreadPriority — Ajuste de prioridade da thread (somente Windows)

  • Descrição: Define a prioridade do worker thread, mapeando diretamente para TThread.Priority no 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").
    • 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.
  • Armadilhas comuns:
    • 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.ElapsedMilliseconds ao 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 WithOnSuccessWithOnError e WithOnTimeout para registrar tempos por cenário.
    • 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/CheckTimeout onde 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 fazSuccess/Complete comemoram/fechamError/Cancel/Timeout informam e recuperam; Terminate garante o reset final.

6. TSafeThread4D — Tabela-Resumo de Callbacks

Callback / Config Thread Quando dispara Uso típico
WithOnInitializeUI (Synchronize)Antes do OnExecuteValidar UI, preparar ambiente
WithOnInitializeEventUIIdem ao acimaReusar métodos/eventos existentes
WithOnExecuteWorkerDurante execuçãoTrabalho pesado (I/O, CPU, rede)
WithOnSuccessUI (Synchronize)Após execução sem erros/cancel/timeoutAtualizar UI com resultado
WithOnCompleteUI (Synchronize)Após sucesso (se LDoComplete=True)Fechamento opcional, salvar estado
WithOnTerminateUI (Synchronize)Sempre no fim (qualquer resultado)Reabilitar UI, limpeza final
WithOnTerminateEventUICompatível com TThread.OnTerminateIntegração com código legado
WithOnErrorUI (Queue)Quando ocorre exceção no OnExecuteLogar e exibir mensagens de erro
WithOnCancelUI (Synchronize)Quando usuário cancelou (CheckCancel)Avisar cancelamento, rollback parcial
WithOnProgressUI (Queue)Durante OnExecute (com throttle)Atualizar barras de progresso
WithProgressIntervalMsDefine intervalo mínimo entre ReportProgressEvitar excesso de mensagens
WithTimeoutMsDefine tempo máximo para execuçãoCancelar tarefas que demoram demais
WithOnTimeoutUI (Synchronize)Quando CheckTimeout expiraAvisar usuário sobre tempo esgotado
WithHeartbeatIntervalMsPing periódico p/ manter UI vivaEvitar ANR no Android
WithHeartbeatAtiva/desativa heartbeatAlternativa simplificada
WithFreeOnTerminateLiberação automática da threadFire-and-forget
WithCompleteWithErrorPermite rodar OnComplete mesmo em erroCleanup homogêneo
WithThreadPriorityDefine prioridade da threadBalancear responsividade x velocidade
WithThreadNameNomeia thread p/ debuggingLogs, profiling, diagnóstico
WithThreadIdExpõe ID nativo da threadCorrelacionar logs
WithMeasureTimeMede 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 TControl principal 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 SynchronizeQueueANRUI 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.

Sobre o autor

TheCodeNaked

No TheCodeNaked, programar é consequência, não ponto de partida. Antes do código, vem a dúvida, a análise, o contexto. Não seguimos fórmulas — questionamos. Criar software é pensar com clareza. O resto é só digitação.

TheCodeNaked

Criar com clareza. Codificar com intenção.

TheCodeNaked

Ótimo! Você se inscreveu com sucesso.

Bem-vindo de volta! Você acessou com sucesso.

Você se inscreveu com sucesso o TheCodeNaked.

Sucesso! Verifique seu e-mail para acessar com o link mágico.

As suas informações de faturamento foram atualizadas.

Seu pagamento não foi atualizado