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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow