Embarcadero Delphi
Kör en tråd medan du håller GUI lyhörd
Sök…
Responsive GUI med hjälp av trådar för bakgrundsarbete och PostMessage för att rapportera tillbaka från trådarna
För att hålla ett GUI lyhört under körning av en lång process krävs antingen några mycket detaljerade "återuppringningar" för att låta GUI bearbeta meddelandekön, eller använda (bakgrund) (arbetar) trådar.
Att sparka bort ett antal trådar för att göra lite arbete är vanligtvis inte ett problem. Det roliga börjar när du vill göra GUI att visa mellanliggande och slutliga resultat eller rapportera om framstegen.
Att visa allt i GUI kräver interaktion med kontroller och / eller meddelandekön / pumpen. Det bör alltid göras i samband med huvudtråden. Aldrig i samband med någon annan tråd.
Det finns många sätt att hantera detta.
Detta exempel visar hur du kan göra det med enkla trådar, vilket gör att GUI kan komma åt trådinstansen efter att den är klar genom att ställa FreeOnTerminate
till false
och rapportera när en tråd är "klar" med PostMessage
.
Kommentarer om rasförhållanden: Hänvisningar till arbetartrådarna förvaras i en matris i formen. När en tråd är klar får motsvarande referens i matrisen noll-ed.
Detta är en potentiell källa till rasförhållanden. Liksom användningen av en "köra" booleska för att göra det lättare att avgöra om det fortfarande finns några trådar som måste slutföras.
Du måste bestämma om du behöver skydda denna resurs med lås eller inte.
I det här exemplet finns det inget behov. De modifieras endast på två platser: StartThreads
metoden och HandleThreadResults
metoden. Båda metoderna körs endast i samband med huvudtråden. Så länge du håller det så och inte börjar kalla dessa metoder ur sammanhanget av olika trådar, finns det inget sätt för dem att producera rasförhållanden.
Tråd
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;
Konstruktören ställer bara in de privata medlemmarna och ställer FreeOnTerminate till False. Detta är viktigt eftersom det gör det möjligt för huvudtråden att fråga efter trådinstansen för dess resultat.
Den exekverande metoden gör sin beräkning och skickar sedan ett meddelande till handtaget som den fick i sin konstruktör för att säga att det är gjort:
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;
Användningen av PostMessage
är väsentlig i detta exempel. PostMessage
"bara" sätter ett meddelande i kön för huvudtrådens meddelandepump och väntar inte på att det ska hanteras. Det är asynkron till sin natur. Om du skulle använda SendMessage
kodar du dig själv i en pickle. SendMessage
sätter meddelandet i kön och väntar tills det har behandlats. Kort sagt, det är synkront.
Deklarationerna för det anpassade UM_WORKERDONE-meddelandet deklareras som:
const
UM_WORKERDONE = WM_APP + 1;
type
TUMWorkerDone = packed record
Msg: Cardinal;
ThreadHandle: Integer;
unused: Integer;
Result: LRESULT;
end;
UM_WORKERDONE
const använder WM_APP
som en utgångspunkt för dess värde för att säkerställa att den inte stör de värden som används av Windows eller Delphi VCL (som rekommenderas av MicroSoft).
Form
Vilken form som helst kan användas för att starta trådar. Allt du behöver göra är att lägga till följande medlemmar till det:
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;
Åh, och exempelkoden antar att det finns ett Memo1: TMemo;
i formulärets deklarationer, som det använder för "loggning och rapportering".
FRunning
kan användas för att förhindra att användargränssnittet börjar klicka medan arbetet pågår. FThreads
används för att hålla instanspekaren och handtaget på de skapade trådarna.
Proceduren för att starta trådarna har en ganska enkel implementering. Det börjar med en kontroll om det redan finns en uppsättning trådar som väntar på. I så fall går det bara ut. Om inte sätter den flaggan till sant och startar trådarna som ger var och en sitt eget handtag så att de vet var de ska posta sitt "gjort" meddelande.
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;
Trådens handtag läggs också i matrisen eftersom det är det vi får i meddelandena som säger att en tråd är gjord och att ha den utanför trådens instans gör det lättare att komma åt. Att ha handtaget tillgängligt utanför trådens instans gör det också möjligt för oss att använda FreeOnTerminate
till True
om vi inte behövde instansen för att få dess resultat (till exempel om de hade lagrats i en databas). I så fall skulle det naturligtvis inte behöva hålla en hänvisning till förekomsten.
Det roliga är i implementeringen av 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;
Denna metod tittar först på tråden med handtaget som mottagits i meddelandet. Om en matchning hittades hämtar den och rapporterar trådens resultat med hjälp av instansen ( FreeOnTerminate
var False
, kom ihåg?) Och slutar sedan: frigör instansen och ställer in både instansreferensen och handtaget till noll, vilket indikerar att den här tråden inte är längre relevant.
Slutligen kontrollerar det om någon av trådarna fortfarande körs. Om ingen hittas rapporteras "allt gjort" och FRunning
flaggan är inställd på False
så att ett nytt parti arbete kan startas.