TheCodeNaked

SafeThread4D — Mini-README (Copy)

1) Instalação e ideia central

  • Inclua SafeThread4D.pas no projeto.
  • Crie os parâmetros fluentes (TSafeThread4DParams.New), configure callbacks e chame:
    • TSafeThread4D.StartThread(Params) ou
    • TSafeThread4D.StartThreadWithWeakRef(Params, ParamsRaw, FParams)(padrão recomendado p/ evitar ciclos de retenção ao usar closures).

2) Guia rápido (snippet base)

var
  P: ISafeThread4DParams;
  ParamsRaw: Pointer;           // weak snapshot (sem AddRef)
  FParams: ISafeThread4DParams; // strong (lifetime + cancel/UI)
begin
  P :=
    TSafeThread4DParams.New
      .WithThreadName('Work')
      .WithMeasureTime(True)
      .WithFreeOnTerminate(True)
      .WithProgressIntervalMs(100)
      .WithOnInitialize(
        procedure(C: TThreadContext)
        begin
          // Preparar UI
          btnStart.Enabled := False;
          ProgressBar.Value := 0;
        end)
      .WithOnProgress(
        procedure(Pct: Single)
        begin
          ProgressBar.Value := Pct * 100;
          lblPct.Text := Format('%.0f%%', [Pct*100]);
        end)
      .WithOnSuccess(
        procedure(C: TThreadContext)
        begin
          MemoLog.Lines.Add('[Success] Ok');
        end)
      .WithOnError(
        procedure(const Msg: string; const C: TThreadContext)
        begin
          MemoLog.Lines.Add('[Error] ' + Msg);
        end)
      .WithOnCancel(
        procedure(C: TThreadContext)
        begin
          MemoLog.Lines.Add('[Cancel] User cancelled');
        end)
      .WithOnTerminate(
        procedure(C: TThreadContext)
        begin
          btnStart.Enabled := True;
          MemoLog.Lines.Add(Format('[Done] %.3f s',
          [C.ElapsedMilliseconds/1000]));
          FParams := nil; // libera reentrância
        end)
      .WithOnExecute(
        procedure(C: TThreadContext)
        var
          i, Total, Step: Integer;
          Params: ISafeThread4DParams;
        begin
          // Weak -> Strong para uso dentro do worker
          Params := ISafeThread4DParams(IInterface(ParamsRaw));
          if Params=nil then raise Exception.Create('ParamsRaw not set');

          Total := 1000000;
          Step  := Total div 20; if Step=0 then Step := 1;

          TSafeThread4D.CheckCancel(Params, C);
          for i := 1 to Total do
          begin
            // checkpoints leves
            if (i and $3FF)=0 then
              TSafeThread4D.CheckCancel(Params, C);

            // ... trabalho ...

            if ((i mod Step)=0) or (i=Total) then
              TSafeThread4D.ReportProgress(Params, i/Total);
          end;
          // (não precisa forçar 100% aqui; a classe faz isso em 
          OnSuccess/OnComplete)
        end);

  // Inicia (atalho encapsulado do padrão Weak+Strong)
  TSafeThread4D.StartThreadWithWeakRef(P, ParamsRaw, FParams);
end;

3) “Do / Avoid”

Do

  • Use WithOnProgress para atualizar UI (barra, labels).
  • Chame CheckCancel/CheckTimeout em loops quentes com cadência bitwise (i and $3FF)=0.
  • Escreva em UI somente dentro dos callbacks (UI-safe).
  • Para “100% final”, confie no comportamento automático: a classe dispara ReportProgress(1.0, True) antes de OnSuccess e em OnComplete (quando aplicável).

Avoid

  • Capturar Params diretamente em closures (risco de retain cycle). Use StartThreadWithWeakRef.
  • Chamar WaitFor na thread de UI (bloqueia a aplicação).
  • Usar Application.ProcessMessages (a classe já sincroniza onde precisa).

4) Pausa cooperativa (integrada)

Você já usa TEvent no app. Abaixo um padrão simples para integrar:

No form (estado e botões)

var
  FInsertPauseEvent: TEvent;     // criado em FormCreate: TEvent.Create(nil, True, True, '');
  FPaused: Boolean;

procedure TForm1.btnPauseClick(Sender: TObject);
begin
  if not Assigned(FInsertRecordsParams) then Exit;

  FPaused := not FPaused;
  if FPaused then begin
    FInsertPauseEvent.ResetEvent;   // Pausa
    btnPause.Text := 'Resume';
    MemoLog.Lines.Add('[User] Operation paused');
  end else begin
    FInsertPauseEvent.SetEvent;     // Resume
    btnPause.Text := 'Pause';
    MemoLog.Lines.Add('[User] Operation resumed');
  end;
end;

No OnExecute (checagem cooperativa)

// dentro do loop no worker:
if (i and $3FF)=0 then
begin
  // 1) pausa cooperativa
  if Assigned(FInsertPauseEvent) then
    FInsertPauseEvent.WaitFor(INFINITE); // retorna imediatamente se em "resume"

  // 2) cancel cooperativo
  TSafeThread4D.CheckCancel(Params, C);
end;
Dica: se quiser que “pause” não bloqueie o worker, troque por:

5) Cancelamento e estado da thread (helpers novos)

// cancelar
TSafeThread4D.Cancel(FParams);

// cancelar e aguardar término (NÃO use em UI a menos que esteja certo)
TSafeThread4D.CancelAndWait(FParams);

// checar se está rodando
if TSafeThread4D.IsThreadRunning(FParams) then
  MemoLog.Lines.Add('Ainda processando...');
IsThreadRunning também tem overload para TThread.

6) Timeout cooperativo

P := TSafeThread4DParams.New
      .WithTimeoutMs(10_000)
      .WithOnTimeout(
        procedure(C: TThreadContext)
        begin
          MemoLog.Lines.Add('[Timeout] Budget estourado');
        end)
      .WithOnExecute(
        procedure(C: TThreadContext)
        var Params: ISafeThread4DParams; i: Integer;
        begin
          Params := ISafeThread4DParams(IInterface(ParamsRaw));
          for i := 1 to N do
          begin
            if (i and $3FF)=0 then
              TSafeThread4D.CheckTimeout(Params, C); // mede contra
              C.StartTick
            // ...
          end;
        end);

7) Heartbeat (ANR awareness)

.WithHeartbeatIntervalMs(300)
.WithOnHeartbeat(
  procedure
  var blip: Char;
  begin
    if Odd(TThread.GetTickCount64 div 300) then blip := '●' else blip := '◦';
    lblStatus.Text := Format('[HB] UI ping %s  %s', [blip, FormatDateTime('hh:nn:ss', Now)]);
  end)
  • O heartbeat manda um Queue periódico para a UI; bom indicativo de “respiração”.
  • Ele não resolve UI bloqueada por trabalho pesado na própria UI.

8) Canal de resultado (exemplo prático)

Há várias formas. Abaixo duas:

(A) TStream em campo do form (já usado no seu demo)

  • Worker grava em FInsertSnapshot: TMemoryStream.
  • Em OnSuccess, você aplica o snapshot na UI.
// OnExecute (worker): salvar resultado
FInsertSnapshot := TMemoryStream.Create;
PrivateMemTable.SaveToStream(FInsertSnapshot);

// OnSuccess (UI): aplicar
FDMemTable.DisableControls;
try
  FDMemTable.Close;
  FInsertSnapshot.Position := 0;
  FDMemTable.LoadFromStream(FInsertSnapshot);
finally
  FDMemTable.EnableControls;
  FDMemTable.Last;
  FreeAndNil(FInsertSnapshot); // liberar buffer
end;

(B) IInterface no Context (genérico)

  • Use um “holder” simples para passar qualquer resultado.
type
  IResultHolder = interface
    ['{D9B8A7A1-...}']
    function GetValue: IInterface;
    procedure SetValue(const V: IInterface);
    property Value: IInterface read GetValue write SetValue;
  end;

  TResultHolder = class(TInterfacedObject, IResultHolder)
  private FValue: IInterface;
  public
    function GetValue: IInterface; begin Result := FValue; end;
    procedure SetValue(const V: IInterface); begin FValue := V; end;
  end;

// No OnExecute:
var Holder: IResultHolder;
begin
  Holder := TResultHolder.Create;
  Holder.Value := TStringList.Create as IInterface; // exemplo
  (Holder.Value as TStringList).Add('dados...');
  // Anexe ao Context via algum campo exposto (se decidir estender TThreadContext)
  // ou publique via variável do form como no modelo (A).
end;
Observação: sua classe hoje não tem um campo Result no TThreadContext. Se isso te interessa, dá para adicionar um ResultObj: IInterface ao record e getters/setters correspondentes nos Params (é uma extensão simples).

9) Política de 100% (como funciona)

  • A classe chama ReportProgress(1.0, True) antes de OnSuccess.
  • Se OnSuccess não rodar mas houver término “limpo” (sem cancel/erro/timeout) e LDoComplete=TrueOnCompletetambém força 100%.
  • Em cancel/timeout/erro com LDoComplete=Falsenão força 100% (UX honesta).

10) Dúvidas comuns

Posso chamar ReportProgress(1.0) manualmente?
Pode, mas não precisa. O 100% final já vem automático nos caminhos de sucesso/complete.

Onde coloco ReportProgress?
No worker (OnExecute), de tempos em tempos (checkpoint), de preferência aliado ao throttle.

Por que Weak+Strong?
Para evitar que a closure segure Params e isso segure a thread/objetos num ciclo de retenção. Use StartThreadWithWeakRef.


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