Szukaj…


Responsywny GUI wykorzystujący wątki do pracy w tle i PostMessage do raportowania z wątków

Utrzymanie responsywnego interfejsu GUI podczas uruchamiania długiego procesu wymaga albo bardzo skomplikowanych „wywołań zwrotnych”, aby umożliwić GUI przetworzenie kolejki komunikatów, lub użycia wątków (roboczych) (roboczych).

Wyrzucenie dowolnej liczby wątków w celu wykonania pracy zwykle nie stanowi problemu. Zabawa zaczyna się, gdy chcesz, aby GUI pokazywał wyniki pośrednie i końcowe lub raportował postępy.

Pokazywanie czegokolwiek w GUI wymaga interakcji z kontrolkami i / lub kolejką komunikatów / pompą. Powinno to zawsze odbywać się w kontekście głównego wątku. Nigdy w kontekście żadnego innego wątku.

Jest na to wiele sposobów.

Ten przykład pokazuje, jak możesz to zrobić za pomocą prostych wątków, umożliwiając GUI dostęp do instancji wątku po zakończeniu, ustawiając FreeOnTerminate na false , oraz raportowanie, kiedy wątek jest „zrobiony” za pomocą PostMessage .

Uwagi dotyczące warunków wyścigu: Odniesienia do wątków roboczych są przechowywane w tablicy w formularzu. Po zakończeniu wątku odpowiadające mu odwołanie w tablicy jest zerowane.

Jest to potencjalne źródło warunków wyścigu. Podobnie jak użycie „działającej” wartości logicznej, aby ułatwić ustalenie, czy są jeszcze jakieś wątki, które należy zakończyć.

Musisz zdecydować, czy chcesz chronić te zasoby za pomocą zamków, czy nie.

W tym przykładzie w obecnej postaci nie ma takiej potrzeby. Są one modyfikowane tylko w dwóch lokalizacjach: metoda StartThreads i metoda HandleThreadResults . Obie metody działają zawsze tylko w kontekście głównego wątku. Tak długo, jak utrzymasz to w ten sposób i nie zaczniesz wywoływać tych metod z kontekstu różnych wątków, nie będzie możliwości stworzenia warunków wyścigu.

Wątek

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;

Konstruktor ustawia tylko członków prywatnych i ustawia FreeOnTerminate na False. Jest to niezbędne, ponieważ pozwoli wątkowi głównemu zapytać instancję wątku o wynik.

Metoda wykonywania wykonuje obliczenia, a następnie wysyła komunikat do uchwytu, który otrzymał w swoim konstruktorze, aby powiedzieć, że została wykonana:

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;

W tym przykładzie niezbędne jest użycie PostMessage . PostMessage „just” umieszcza komunikat w kolejce pompy komunikatów głównego wątku i nie czeka na obsłużenie. Ma charakter asynchroniczny. Gdybyś używał SendMessage , SendMessage się w marynacie. SendMessage umieszcza komunikat w kolejce i czeka, aż zostanie przetworzony. Krótko mówiąc, jest synchroniczny.

Deklaracje niestandardowej wiadomości UM_WORKERDONE są deklarowane jako:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

UM_WORKERDONE używa WM_APP jako punktu początkowego dla swojej wartości, aby upewnić się, że nie koliduje z żadnymi wartościami używanymi przez Windows lub Delphi VCL (zgodnie z zaleceniami MicroSoft).

Formularz

Do uruchomienia wątków można użyć dowolnej formy. Wszystko, co musisz zrobić, to dodać do niego następujących członków:

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;

Aha, a przykładowy kod zakłada istnienie Memo1: TMemo; w deklaracjach formularza, które wykorzystuje do „rejestrowania i raportowania”.

FRunning może być użyty, aby zapobiec kliknięciu GUI podczas pracy. FThreads służy do przechowywania wskaźnika instancji i uchwytu utworzonych wątków.

Procedura uruchamiania wątków ma dość prostą implementację. Zaczyna się od sprawdzenia, czy jest już oczekiwany zestaw wątków. Jeśli tak, to po prostu wychodzi. Jeśli nie, ustawia flagę na true i uruchamia wątki, zapewniając każdemu z nich własny uchwyt, aby wiedzieli, gdzie opublikować komunikat „gotowe”.

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;

Uchwyt wątku jest również umieszczany w tablicy, ponieważ to, co otrzymujemy w wiadomościach, które mówią nam, że wątek jest zakończony, a umieszczenie go poza instancją wątku ułatwia dostęp. Posiadanie uchwytu dostępnego poza instancją wątku pozwala nam również użyć FreeOnTerminate ustawionego na True jeśli nie potrzebowaliśmy instancji, aby uzyskać jej wyniki (na przykład, jeśli były one przechowywane w bazie danych). W takim przypadku oczywiście nie byłoby potrzeby utrzymywania odniesienia do instancji.

Zabawa polega na implementacji 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;

Ta metoda najpierw wyszukuje wątek za pomocą uchwytu otrzymanego w wiadomości. Jeśli znaleziono dopasowanie, pobiera i raportuje wynik wątku za pomocą instancji ( FreeOnTerminate był False , pamiętasz?), A następnie kończy: zwolnienie instancji i ustawienie zarówno odwołania do instancji, jak i uchwytu na zero, wskazując, że ten wątek jest nie dłużej istotne.

Na koniec sprawdza, czy któryś z wątków nadal działa. Jeśli nic nie zostanie znalezione, raportowane jest „wszystko zrobione”, a flaga FRunning ustawiona na False dzięki czemu można rozpocząć nową partię pracy.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow