1) Instalação e ideia central
- Inclua
SafeThread4D.pasno projeto. - Crie os parâmetros fluentes (
TSafeThread4DParams.New), configure callbacks e chame:TSafeThread4D.StartThread(Params)ouTSafeThread4D.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
WithOnProgresspara atualizar UI (barra, labels). - Chame
CheckCancel/CheckTimeoutem 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 deOnSuccesse emOnComplete(quando aplicável).
Avoid
- Capturar
Paramsdiretamente em closures (risco de retain cycle). UseStartThreadWithWeakRef. - Chamar
WaitForna 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...');
IsThreadRunningtambém tem overload paraTThread.
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
Queueperió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 campoResultnoTThreadContext. Se isso te interessa, dá para adicionar umResultObj: IInterfaceao 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 deOnSuccess. - Se
OnSuccessnão rodar mas houver término “limpo” (sem cancel/erro/timeout) eLDoComplete=True,OnCompletetambém força 100%. - Em cancel/timeout/erro com
LDoComplete=False, nã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.