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.