Embarcadero Delphi
Een thread uitvoeren terwijl de GUI responsief blijft
Zoeken…
Responsieve GUI die threads gebruikt voor achtergrondwerk en PostMessage om terug te rapporteren vanuit de threads
Om een GUI responsief te houden tijdens het uitvoeren van een langdurig proces, zijn enkele zeer uitgebreide "callbacks" vereist om de GUI in staat te stellen zijn berichtenwachtrij te verwerken, of het gebruik van (achtergrond) (werk) threads.
Een aantal threads aftrappen om wat werk te doen, is meestal geen probleem. Het plezier begint wanneer u de GUI tussenresultaten en eindresultaten wilt laten weergeven of over de voortgang wilt rapporteren.
Alles weergeven in de GUI vereist interactie met bedieningselementen en / of de berichtenwachtrij / pomp. Dat moet altijd in de context van de rode draad worden gedaan. Nooit in de context van een andere thread.
Er zijn veel manieren om hiermee om te gaan.
Dit voorbeeld laat zien hoe u dit kunt doen met behulp van eenvoudige threads, waardoor de GUI toegang krijgt tot de threadinstantie nadat deze is voltooid door FreeOnTerminate
op false
in te stellen en te rapporteren wanneer een thread is "gedaan" met PostMessage
.
Opmerkingen over raceomstandigheden: verwijzingen naar de werkthreads worden in een reeks in de vorm bewaard. Wanneer een thread is voltooid, wordt de overeenkomstige referentie in de array nul.
Dit is een potentiële bron van raceomstandigheden. Net als het gebruik van een "Running" Boolean om het eenvoudiger te maken om te bepalen of er nog steeds threads zijn die moeten worden voltooid.
U moet beslissen of u deze bron moet beschermen met sloten of niet.
In dit voorbeeld is het op dit moment niet nodig. Ze worden slechts op twee locaties aangepast: de StartThreads
methode en de HandleThreadResults
methode. Beide methoden worden alleen uitgevoerd in de context van de hoofdthread. Zolang je het zo houdt en deze methoden niet vanuit de context van verschillende threads begint aan te roepen, is er geen manier voor hen om race-omstandigheden te produceren.
Draad
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;
De constructor stelt alleen de privéleden in en stelt FreeOnTerminate in op False. Dit is essentieel omdat hiermee de hoofdthread de threadinstantie kan opvragen voor het resultaat.
De uitvoeringsmethode voert de berekening uit en plaatst vervolgens een bericht op de handle die het in de constructor heeft ontvangen om te zeggen dat het klaar is:
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;
Het gebruik van PostMessage
is essentieel in dit voorbeeld. PostMessage
"gewoon" een bericht in de wachtrij van de berichtenpomp van de PostMessage
en wacht niet tot het wordt afgehandeld. Het is asynchroon van aard. Als je SendMessage
zou gebruiken, codeer je jezelf in een augurk. SendMessage
plaatst het bericht in de wachtrij en wacht totdat het is verwerkt. Kortom, het is synchroon.
De aangiften voor het aangepaste UM_WORKERDONE-bericht worden gedeclareerd als:
const
UM_WORKERDONE = WM_APP + 1;
type
TUMWorkerDone = packed record
Msg: Cardinal;
ThreadHandle: Integer;
unused: Integer;
Result: LRESULT;
end;
Het UM_WORKERDONE
const gebruikt WM_APP
als uitgangspunt voor zijn waarde om te zorgen dat het niet interfereert met waarden die worden gebruikt door Windows of de Delphi VCL (zoals aanbevolen door MicroSoft).
Het formulier
Elke vorm kan worden gebruikt om threads te starten. Het enige wat u hoeft te doen is de volgende leden toevoegen:
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, en de voorbeeldcode veronderstelt het bestaan van een Memo1: TMemo;
in de verklaringen van het formulier, die het gebruikt voor "logging en rapportage".
De FRunning
kan worden gebruikt om te voorkomen dat op de GUI wordt geklikt terwijl het werk bezig is. FThreads
wordt gebruikt om de instantie-aanwijzer en de greep van de gemaakte threads vast te houden.
De procedure om de threads te starten heeft een vrij eenvoudige implementatie. Het begint met een controle of er al op een reeks threads wordt gewacht. Als dat zo is, wordt het gewoon afgesloten. Als dit niet het geval is, wordt de vlag ingesteld op true en worden de threads gestart die elk een eigen handvat hebben, zodat ze weten waar ze hun "klaar" bericht moeten posten.
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;
Het handvat van de thread wordt ook in de array geplaatst, want dat is wat we ontvangen in de berichten die ons vertellen dat een thread is gedaan en door het buiten de instantie van de thread te plaatsen, is het iets gemakkelijker toegankelijk. Als de handle beschikbaar is buiten de instantie van de thread, kunnen we FreeOnTerminate
instellen op True
als we de instantie niet nodig hadden om de resultaten te krijgen (bijvoorbeeld als ze in een database waren opgeslagen). In dat geval is het natuurlijk niet nodig om een verwijzing naar de instantie te houden.
Het leuke is in de implementatie van 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;
Bij deze methode wordt eerst de thread opgezocht met behulp van de greep die in het bericht is ontvangen. Als er een overeenkomst is gevonden, haalt deze het resultaat van de thread op en rapporteert deze met behulp van de instantie ( FreeOnTerminate
was False
, weet je nog?), En eindigt vervolgens: de instantie vrijmaken en zowel de instantie-referentie als de handle op nul zetten, wat aangeeft dat deze draad geen is langer relevant.
Ten slotte wordt gecontroleerd of een van de threads nog steeds actief is. Als er geen wordt gevonden, wordt "alles klaar" gerapporteerd en wordt de vlag FRunning
ingesteld op False
zodat een nieuwe batch werk kan worden gestart.