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.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow