Suche…


Responsive GUI mit Threads für die Hintergrundarbeit und PostMessage zum Berichten von Threads

Um eine GUI während eines langwierigen Prozesses reaktionsfähig zu halten, sind entweder sehr aufwendige "Rückrufe" erforderlich, um der GUI die Verarbeitung ihrer Nachrichtenwarteschlange zu ermöglichen, oder die Verwendung von (Hintergrund) (Worker) -Threads.

Es ist normalerweise kein Problem, eine beliebige Anzahl von Threads zu starten, um einige Arbeit zu erledigen. Der Spaß beginnt, wenn Sie die GUI Zwischen- und Endergebnisse anzeigen oder über den Fortschritt berichten möchten.

Das Anzeigen von Elementen in der GUI erfordert die Interaktion mit Steuerelementen und / oder der Nachrichtenwarteschlange / -pumpe. Dies sollte immer im Kontext des Hauptthreads erfolgen. Niemals im Zusammenhang mit einem anderen Thread.

Es gibt viele Möglichkeiten, damit umzugehen.

Dieses Beispiel zeigt, wie Sie dies mit einfachen Threads tun können. Dadurch kann die GUI auf die Thread-Instanz zugreifen, nachdem sie fertig ist, indem Sie FreeOnTerminate auf false und melden, wenn ein Thread mit PostMessage "fertig" PostMessage .

Hinweise zu den Rennbedingungen: Verweise auf die Arbeitsthreads werden im Formular in einem Array gespeichert. Wenn ein Thread abgeschlossen ist, wird die entsprechende Referenz im Array auf Null gesetzt.

Dies ist eine potenzielle Quelle für Rennbedingungen. Die Verwendung eines booleschen Typs "Running" erleichtert die Feststellung, ob noch Threads vorhanden sind, die abgeschlossen werden müssen.

Sie müssen entscheiden, ob Sie diese Ressource mit Sperren schützen müssen oder nicht.

In diesem Beispiel ist es so, wie es ist, keine Notwendigkeit. Sie werden nur an zwei Stellen geändert: der StartThreads Methode und der HandleThreadResults Methode. Beide Methoden laufen immer nur im Kontext des Hauptthreads. Solange Sie es auf diese Weise beibehalten und diese Methoden nicht aus dem Kontext verschiedener Threads aufrufen, gibt es für sie keine Möglichkeit, Race-Bedingungen zu erzeugen.

Faden

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;

Der Konstruktor setzt nur die privaten Mitglieder und setzt FreeOnTerminate auf False. Dies ist wichtig, da der Haupt-Thread die Thread-Instanz nach seinem Ergebnis abfragen kann.

Die Ausführungsmethode führt ihre Berechnung aus und sendet dann eine Nachricht an das Handle, das sie in ihrem Konstruktor erhalten hat, um zu sagen, dass sie fertig ist:

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;

Die Verwendung von PostMessage ist in diesem Beispiel unerlässlich. PostMessage "nur" eine Nachricht in die Warteschlange der Nachrichtenpumpe des Hauptthreads ein und wartet nicht darauf, dass sie verarbeitet wird. Es ist asynchron in der Natur. Wenn Sie SendMessage verwenden SendMessage , würden Sie sich selbst in eine Pickle codieren. SendMessage legt die Nachricht in die Warteschlange und wartet, bis sie verarbeitet wurde. Kurz gesagt, es ist synchron.

Die Deklarationen für die benutzerdefinierte UM_WORKERDONE-Nachricht werden wie folgt deklariert:

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

Die UM_WORKERDONE verwendet WM_APP als Ausgangspunkt für ihren Wert, um sicherzustellen, dass keine von Windows oder der Delphi-VCL (von MicroSoft empfohlen ) verwendeten Werte beeinträchtigt werden.

Bilden

Jedes Formular kann zum Starten von Threads verwendet werden. Sie müssen lediglich die folgenden Mitglieder hinzufügen:

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, und der Beispielcode setzt die Existenz eines Memo1: TMemo; in den Deklarationen des Formulars, die es für "Protokollierung und Berichterstellung" verwendet.

Mit dem FRunning kann verhindert werden, dass die GUI während der Arbeit angeklickt wird. FThreads wird verwendet, um den Instanzzeiger und das Handle der erstellten Threads zu halten.

Die Prozedur zum Starten der Threads ist ziemlich unkompliziert. Es beginnt mit einer Prüfung, ob bereits ein Satz Threads gewartet wird. Wenn ja, wird es einfach beendet. Ist dies nicht der Fall, wird das Flag auf true gesetzt und die Threads werden mit einem eigenen Handle versehen, sodass sie wissen, wo sie ihre "Fertig" -Meldung posten sollen.

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;

Das Handle des Threads wird auch in das Array eingefügt, weil wir dies in den Nachrichten erhalten, die uns sagen, dass ein Thread fertiggestellt ist. Wenn er sich außerhalb der Instanz des Threads befindet, ist der Zugriff etwas einfacher. Wenn das Handle außerhalb der Instanz des Threads verfügbar ist, können wir auch FreeOnTerminate auf True setzen, wenn die Instanz nicht benötigt wurde, um die Ergebnisse zu erhalten (z. B. wenn sie in einer Datenbank gespeichert wurden). In diesem Fall wäre es natürlich nicht nötig, einen Hinweis auf die Instanz zu führen.

Der Spaß liegt in der HandleThreadResult-Implementierung:

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;

Diese Methode sucht zuerst den Thread anhand des in der Nachricht empfangenen Handles. Wenn eine Übereinstimmung gefunden wurde, ruft es das Ergebnis des Threads mit der Instanz ab ( FreeOnTerminate war False , erinnern Sie sich daran?) Und meldet das Ergebnis des Threads. Anschließend wird es beendet: Die Instanz wird FreeOnTerminate und der FreeOnTerminate und das Handle auf null gesetzt länger relevant.

Zum Schluss wird geprüft, ob noch Threads laufen. Wenn keine gefunden wird, wird "all done" gemeldet und das FRunning Flag auf " False " gesetzt, damit ein neuer Arbeitsstapel gestartet werden kann.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow