Embarcadero Delphi
Einen Thread ausführen, während die GUI ansprechend bleibt
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.