Delphi

Un thread che non serve a niente: l'errore nascosto in Synchronize

calendar_today personTeam EGSOFT schedule3 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

Capita di trovare online (o di scrivere di getto) codice che avvolge un'operazione su database in un TThread per non bloccare l'interfaccia, ma che poi esegue tutto il lavoro dentro Synchronize. Il risultato è un thread che gira, ma che di fatto blocca comunque il thread principale per l'intera durata della query: l'effetto pratico è identico a non aver usato affatto un thread, con la complessità in più di doverlo gestire.

Il codice che sembra corretto

A un primo sguardo questa classe sembra fare le cose per bene: eredita da TThread, ha FreeOnTerminate impostato, e centralizza la copia dei dati in un metodo dedicato.

type
  TQueryToMemTableThread = class(TThread)
  private
    FQuery: TFDQuery;
    FMemTable: TFDMemTable;
    procedure CopiaDati;
  protected
    procedure Execute; override;
  public
    constructor Create(AQuery: TFDQuery; AMemTable: TFDMemTable);
  end;

constructor TQueryToMemTableThread.Create(AQuery: TFDQuery; AMemTable: TFDMemTable);
begin
  inherited Create(True);
  FreeOnTerminate := True;
  FQuery := AQuery;
  FMemTable := AMemTable;
end;

procedure TQueryToMemTableThread.CopiaDati;
begin
  FQuery.Open;                    // interroga il database...
  FMemTable.CopyDataSet(FQuery, [coStructure, coRestart, coAppend]);
  FQuery.Close;
end;

procedure TQueryToMemTableThread.Execute;
begin
  Synchronize(CopiaDati);         // ...ma lo fa tutto dentro Synchronize!
end;

Il problema è tutto in quell'ultima riga: Synchronize sospende il thread secondario ed esegue CopiaDati — apertura della query compresa — sul thread principale. La UI resta bloccata esattamente come se la query fosse stata aperta direttamente in un click handler.

La versione corretta

Il principio da seguire è: tutto ciò che è pesante (l'interrogazione al database) resta nel thread secondario; solo il passaggio finale che tocca componenti legati alla UI va marshallato sul thread principale, e possibilmente in modo asincrono con Queue invece che con Synchronize bloccante.

procedure TQueryToMemTableThread.Execute;
var
  DatiLocali: TFDMemTable;
begin
  // Il thread secondario deve usare una propria connessione FireDAC,
  // non quella condivisa con il form (FDConnection non è thread-safe
  // se usata contemporaneamente da più thread).
  DatiLocali := TFDMemTable.Create(nil);
  try
    FQuery.Open;                                   // lavoro pesante, fuori da Synchronize
    DatiLocali.CopyDataSet(FQuery, [coStructure, coRestart, coAppend]);
    FQuery.Close;

    Queue(
      procedure
      begin
        FMemTable.CopyDataSet(DatiLocali, [coStructure, coRestart, coAppend]);
      end);
  finally
    DatiLocali.Free;
  end;
end;

La lezione generale

Ogni volta che si scrive Synchronize vale la pena chiedersi cosa c'è davvero dentro quel blocco: se contiene l'operazione che si voleva spostare fuori dal thread principale, il thread non sta facendo il suo lavoro. La regola pratica è mantenere dentro Synchronize (o meglio Queue, che non blocca il chiamante) solo l'aggiornamento finale di componenti visuali, mai l'accesso a risorse esterne come database o rete.

Altri articoli su Delphi grid_viewTutte le tematiche
Scrivici su WhatsApp