Embarcadero Delphi
Esecuzione di un thread mantenendo la GUI reattiva
Ricerca…
Interfaccia grafica reattiva che utilizza thread per il lavoro in background e PostMessage per segnalare i thread
Mantenere una GUI reattiva durante l'esecuzione di un lungo processo richiede alcune "callback" molto elaborate per consentire alla GUI di elaborare la sua coda di messaggi o l'uso di thread (di background) (worker).
Dare il via a un numero qualsiasi di thread per fare un po 'di lavoro di solito non è un problema. Il divertimento inizia quando vuoi che la GUI mostri i risultati intermedi e finali o riferisca sui progressi.
La visualizzazione di qualsiasi elemento nella GUI richiede l'interazione con i controlli e / o la coda / pompa dei messaggi. Questo dovrebbe sempre essere fatto nel contesto del thread principale. Mai nel contesto di nessun altro thread.
Ci sono molti modi per gestire questo.
Questo esempio mostra come puoi farlo usando semplici thread, permettendo alla GUI di accedere all'istanza del thread dopo che è stata completata impostando FreeOnTerminate
su false
, e segnalando quando un thread è "fatto" usando PostMessage
.
Note sulle condizioni di competizione: i riferimenti ai thread di lavoro sono mantenuti in una matrice nel modulo. Al termine di un thread, il riferimento corrispondente nell'array diventa nullo.
Questa è una potenziale fonte di condizioni di gara. Come è l'uso di un booleano "In esecuzione" per rendere più facile determinare se ci sono ancora dei thread che devono finire.
Avrai bisogno di decidere se hai bisogno di proteggere queste risorse usando le serrature o no.
In questo esempio, così com'è, non ce n'è bisogno. Sono solo modificati in due posizioni: il metodo StartThreads
e il metodo HandleThreadResults
. Entrambi i metodi funzionano sempre nel contesto del thread principale. Finché la si mantiene in questo modo e non si inizia a chiamare questi metodi dal contesto di thread diversi, non è possibile per loro produrre condizioni di gara.
Filo
type
TWorker = class(TThread)
private
FFactor: Double;
FResult: Double;
FReportTo: THandle;
protected
procedure Execute; override;
public
constructor Create(const aFactor: Double; const aReportTo: THandle);
property Factor: Double read FFactor;
property Result: Double read FResult;
end;
Il costruttore imposta semplicemente i membri privati e imposta FreeOnTerminate su False. Questo è essenziale in quanto consente al thread principale di interrogare l'istanza del thread per il suo risultato.
Il metodo execute esegue il suo calcolo e quindi invia un messaggio all'handle che ha ricevuto nel suo costruttore per dire che è stato fatto:
procedure TWorker.Execute;
const
Max = 100000000;var
i : Integer;
begin
inherited;
FResult := FFactor;
for i := 1 to Max do
FResult := Sqrt(FResult);
PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;
L'uso di PostMessage
è essenziale in questo esempio. PostMessage
"just" inserisce un messaggio nella coda del message pump del thread principale e non attende che venga gestito. È di natura asincrona. Se dovessi usare SendMessage
ti staresti codificando in un sottaceto. SendMessage
mette il messaggio in coda e attende fino a quando non è stato elaborato. In breve, è sincrono.
Le dichiarazioni per il messaggio personalizzato UM_WORKERDONE sono dichiarate come:
const
UM_WORKERDONE = WM_APP + 1;
type
TUMWorkerDone = packed record
Msg: Cardinal;
ThreadHandle: Integer;
unused: Integer;
Result: LRESULT;
end;
Il const di UM_WORKERDONE
utilizza WM_APP
come punto di partenza per il suo valore per garantire che non interferisca con i valori utilizzati da Windows o Delphi VCL (come raccomandato da MicroSoft).
Modulo
Qualsiasi forma può essere utilizzata per avviare discussioni. Tutto ciò che devi fare è aggiungere i seguenti membri:
private
FRunning: Boolean;
FThreads: array of record
Instance: TThread;
Handle: THandle;
end;
procedure StartThreads(const aNumber: Integer);
procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;
Oh, e il codice di esempio presuppone l'esistenza di un Memo1: TMemo;
nelle dichiarazioni del modulo, che utilizza per "logging e reporting".
È possibile utilizzare FRunning
per impedire che la GUI venga avviata facendo clic mentre è in corso il lavoro. FThreads
viene utilizzato per contenere il puntatore dell'istanza e l'handle dei thread creati.
La procedura per avviare i thread ha un'implementazione piuttosto semplice. Inizia con un controllo se è già in attesa una serie di thread. Se è così, esce. In caso contrario, imposta flag su true e avvia i thread fornendo ciascuno il proprio handle in modo che sappiano dove pubblicare il messaggio "done".
procedure TForm1.StartThreads(const aNumber: Integer);
var
i: Integer;
begin
if FRunning then
Exit;
FRunning := True;
Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
SetLength(FThreads, aNumber);
for i := 0 to aNumber - 1 do
begin
FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
FThreads[i].Handle := FThreads[i].Instance.Handle;
end;
end;
L'handle del thread viene inserito anche nell'array perché questo è ciò che riceviamo nei messaggi che ci dicono che un thread è stato eseguito e che averlo fuori dall'istanza del thread rende l'accesso leggermente più facile. Avere l'handle disponibile al di fuori dell'istanza del thread ci consente anche di utilizzare FreeOnTerminate
impostato su True
se non abbiamo avuto bisogno dell'istanza per ottenere i suoi risultati (ad esempio se fossero stati memorizzati in un database). In tal caso, non ci sarebbe naturalmente bisogno di mantenere un riferimento all'istanza.
Il divertimento è nell'implementazione di HandleThreadResult:
procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
i: Integer;
ThreadIdx: Integer;
Thread: TWorker;
Done: Boolean;
begin
// Find thread in array
ThreadIdx := -1;
for i := Low(FThreads) to High(FThreads) do
if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
begin
ThreadIdx := i;
Break;
end;
// Report results and free the thread, nilling its pointer and handle
// so we can detect when all threads are done.
if ThreadIdx > -1 then
begin
Thread := TWorker(FThreads[i].Instance);
Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
FreeAndNil(FThreads[i].Instance);
FThreads[i].Handle := nil;
end;
// See whether all threads have finished.
Done := True;
for i := Low(FThreads) to High(FThreads) do
if Assigned(FThreads[i].Instance) then
begin
Done := False;
Break;
end;
if Done then
begin
Memo1.Lines.Add('Work done');
FRunning := False;
end;
end;
Questo metodo prima cerca il thread usando l'handle ricevuto nel messaggio. Se viene trovata una corrispondenza, recupera e segnala il risultato del thread utilizzando l'istanza ( FreeOnTerminate
era False
, ricorda?), Quindi termina: liberando l'istanza e impostando il riferimento dell'istanza e l'handle su zero, indicando che questa discussione non è più rilevante.
Infine controlla se qualcuno dei thread è ancora in esecuzione. Se non viene trovato nessuno, viene segnalato "tutto fatto" e il flag FRunning
impostato su False
può essere avviato un nuovo lotto di lavoro.