Recherche…


Interface graphique réactive utilisant des threads pour le travail en arrière-plan et PostMessage pour générer des rapports à partir des threads

Garder une interface graphique réactive lors de l'exécution d'un processus long nécessite soit des "callbacks" très élaborés pour permettre à l'interface graphique de traiter sa file d'attente de messages, soit l'utilisation de threads (en arrière-plan) (worker).

Lancer n'importe quel nombre de threads pour faire un travail n'est généralement pas un problème. Le plaisir commence lorsque vous souhaitez que l'interface graphique affiche les résultats intermédiaires et finaux ou que vous décriviez les progrès.

L'affichage de tout élément dans l'interface graphique nécessite une interaction avec les contrôles et / ou la file d'attente / pompe de messages. Cela devrait toujours être fait dans le contexte du thread principal. Jamais dans le contexte d'un autre fil.

Il y a plusieurs façons de gérer cela.

Cet exemple montre comment vous pouvez le faire en utilisant des threads simples, permettant à l'interface graphique d'accéder à l'instance de thread une fois celle-ci terminée en définissant FreeOnTerminate sur false et en signalant qu'un thread est "terminé" en utilisant PostMessage .

Remarques sur les conditions de course: Les références aux threads de travail sont conservées dans un tableau du formulaire. Lorsqu'un thread est terminé, la référence correspondante dans le tableau devient nil-ed.

C'est une source potentielle de conditions de course. Tout comme l'utilisation d'un booléen "Running" pour déterminer plus facilement s'il reste des threads à terminer.

Vous devrez décider si vous devez protéger ces ressources à l'aide de verrous ou non.

Dans cet exemple, tel qu'il est, il n'y a pas besoin. Ils ne sont modifiés qu'à deux emplacements: la méthode StartThreads et la méthode HandleThreadResults . Les deux méthodes ne fonctionnent que dans le contexte du thread principal. Tant que vous continuez ainsi et que vous ne commencez pas à appeler ces méthodes dans le contexte de threads différents, il leur est impossible de créer des conditions de course.

Fil

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;

Le constructeur définit simplement les membres privés et définit FreeOnTerminate sur False. Ceci est essentiel car cela permettra au thread principal d'interroger l'instance du thread pour connaître son résultat.

La méthode execute effectue son calcul, puis publie un message dans le descripteur reçu dans son constructeur pour indiquer que c'est fait:

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'utilisation de PostMessage est essentielle dans cet exemple. PostMessage "just" place un message dans la file d'attente de la pompe de messages du thread principal et n'attend pas qu'il soit traité. C'est de nature asynchrone. Si vous deviez utiliser SendMessage vous vous SendMessage dans un cornichon. SendMessage place le message dans la file d'attente et attend qu'il soit traité. En bref, c'est synchrone.

Les déclarations pour le message personnalisé UM_WORKERDONE sont déclarées comme suit:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

Le const UM_WORKERDONE utilise WM_APP comme point de départ pour sa valeur afin de s'assurer qu'il n'interfère pas avec les valeurs utilisées par Windows ou la VCL Delphi (comme recommandé par MicroSoft).

Forme

Toute forme peut être utilisée pour démarrer des threads. Il vous suffit d'y ajouter les membres suivants:

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, et l'exemple de code suppose l'existence d'un Memo1: TMemo; dans les déclarations du formulaire, qu'il utilise pour "journalisation et reporting".

Le FRunning peut être utilisé pour empêcher que l'interface graphique ne commence à être cliquée pendant le travail. FThreads est utilisé pour contenir le pointeur d'instance et le handle des threads créés.

La procédure pour démarrer les threads a une implémentation assez simple. Il commence par vérifier si un ensemble de threads est déjà en attente. Si c'est le cas, il ne fait que sortir. Si ce n'est pas le cas, il attribue la valeur true à l'indicateur et lance les threads en fournissant à chacun son propre handle afin qu'il sache où publier son message "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;

Le handle du thread est également placé dans le tableau car c'est ce que nous recevons dans les messages qui nous indiquent qu'un thread est terminé et qu'il est plus facile à accéder en dehors de l'instance du thread. Avoir le handle disponible en dehors de l'instance du thread nous permet également d'utiliser FreeOnTerminate défini sur True si nous n'avions pas besoin de l'instance pour obtenir ses résultats (par exemple s'ils avaient été stockés dans une base de données). Dans ce cas, il ne serait bien sûr pas nécessaire de garder une référence à l'instance.

Le plaisir réside dans l'implémentation de 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;

Cette méthode recherche d'abord le thread en utilisant le handle reçu dans le message. Si une correspondance a été trouvée, elle récupère et rapporte le résultat du thread en utilisant l'instance ( FreeOnTerminate était False , souvenez-vous?), Puis se termine: libérer l'instance et définir à la fois la référence d'instance et le handle sur nil, indiquant que ce thread n'est pas plus pertinent.

Enfin, il vérifie si l'un des threads est toujours en cours d'exécution. Si aucun n'est trouvé, "all done" est signalé et l'indicateur FRunning défini sur False afin qu'un nouveau lot de travail puisse être démarré.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow