Embarcadero Delphi
Uruchomienie wątku przy jednoczesnym zachowaniu responsywności GUI
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.