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.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow