Przykład puli wątków Delphi przy użyciu AsyncCalls

To mój następny projekt testowy, aby zobaczyć, która biblioteka wątków dla Delphi najlepiej pasuje do mojego zadania „skanowania plików”, które chciałbym przetwarzać w wielu wątkach / w puli wątków.

Aby powtórzyć mój cel: przekształć moje sekwencyjne „skanowanie plików” ponad 500–2000 plików z podejścia niepodzielonego na wątek. Nie powinienem mieć jednocześnie uruchomionych 500 wątków, dlatego chciałbym użyć puli wątków. Pula wątków jest klasą podobną do kolejki, która zasila szereg działających wątków następnym zadaniem z kolejki.

Pierwszą (bardzo podstawową) próbę przeprowadzono po prostu rozszerzając klasę TThread i implementując metodę Execute (mój analizator ciągów wątków).

Ponieważ Delphi nie ma zaimplementowanej klasy puli wątków po wyjęciu z pudełka, w drugiej próbie spróbowałem użyć OmniThreadLibrary Primoz Gabrijelcic.

OTL jest fantastyczny, ma zillion sposobów na uruchomienie zadania w tle, co jest dobrym rozwiązaniem, jeśli chcesz mieć podejście typu „podpal i zapomnij” do obsługi wątkowego wykonywania fragmentów kodu.

instagram viewer

AsyncCalls by Andreas Hausladen

Uwaga: poniższe czynności będą łatwiejsze do zrozumienia, jeśli najpierw pobierzesz kod źródłowy.

Badając kolejne sposoby wykonywania niektórych funkcji w sposób podzielony na wątki, postanowiłem także wypróbować jednostkę „AsyncCalls.pas” opracowaną przez Andreasa Hausladena. Andy's AsyncCalls - Asynchroniczne wywołania funkcji Unit to kolejna biblioteka, z której deweloper Delphi może złagodzić problem implementacji wątkowego podejścia do wykonania jakiegoś kodu.

Z bloga Andy'ego: Dzięki AsyncCalls możesz wykonywać wiele funkcji jednocześnie i synchronizować je w każdym punkcie funkcji lub metody, która je uruchomiła... Jednostka AsyncCalls oferuje różnorodne prototypy funkcji do wywoływania funkcji asynchronicznych... Implementuje pulę wątków! Instalacja jest bardzo łatwa: wystarczy użyć asynchronizacji z dowolnego urządzenia, aby uzyskać natychmiastowy dostęp do rzeczy takich jak „uruchom w osobnym wątku, zsynchronizuj główny interfejs użytkownika, poczekaj, aż zakończysz”.

Oprócz darmowego AsyncCalls (licencja MPL), Andy często publikuje również własne poprawki dla IDE Delphi, takie jak „Delphi Speed ​​Up" i "DDevExtensions„Jestem pewien, że słyszałeś (jeśli jeszcze nie używasz).

AsyncCalls In Action

Zasadniczo wszystkie funkcje AsyncCall zwracają interfejs IAsyncCall, który umożliwia synchronizację funkcji. IAsnycCall udostępnia następujące metody:

 //v 2.98 z asynccalls.pas
IAsyncCall = interfejs
// czeka na zakończenie funkcji i zwraca wartość zwracaną
funkcja Synchronizacja: liczba całkowita;
// zwraca wartość True po zakończeniu funkcji asynchronicznej
funkcja Zakończona: Boolean;
// zwraca wartość zwracaną przez funkcję asynchroniczną, gdy Zakończone ma wartość PRAWDA
funkcja ReturnValue: Integer;
// informuje AsyncCalls, że przypisana funkcja nie może być wykonana w bieżącym trenarze
procedura ForceDifferentThread;
koniec;

Oto przykładowe wywołanie metody oczekującej dwóch parametrów całkowitych (zwracających IAsyncCall):

 TAsyncCalls. Invoke (AsyncMethod, i, Random (500));
funkcjonować TAsyncCallsForm. AsyncMethod (taskNr, sleepTime: integer): integer;
zaczynać
wynik: = sleepTime;
Sleep (sleepTime);
TAsyncCalls. VCLInvoke (
procedura
zaczynać
Log (Format ('done> nr:% d / zadania:% d / slept:% d', [tasknr, asyncHelper. TaskCount, sleepTime]));
koniec);
koniec;

TAsyncCalls. VCLInvoke to sposób na synchronizację z głównym wątkiem (głównym wątkiem aplikacji - interfejsem użytkownika aplikacji). VCLInvoke natychmiast wraca. Anonimowa metoda zostanie wykonana w głównym wątku. Istnieje również VCLSync, który zwraca się, gdy anonimowa metoda została wywołana w głównym wątku.

Pula wątków w AsyncCalls

Powrót do mojego zadania „skanowania plików”: podczas zasilania (w pętli for) pulę wątków asynccalls z serią TAsyncCalls. Wywołania Invoke (), zadania zostaną dodane do wewnętrznej puli i zostaną wykonane „kiedy nadejdzie czas” (kiedy zakończone zostaną wcześniej dodane wywołania).

Poczekaj na zakończenie wszystkich wywołań IAsync

Funkcja AsyncMultiSync zdefiniowana w asnyccalls czeka na zakończenie wywołań asynchronicznych (i innych uchwytów). Jest kilka przeciążony sposoby wywoływania AsyncMultiSync, a oto najprostszy z nich:

funkcjonować AsyncMultiSync (const Lista: tablica IAsyncCall; WaitAll: Boolean = True; Milisekund: Kardynał = NIESKOŃCZONY): Kardynał; 

Jeśli chcę mieć zaimplementowane „poczekaj wszystko”, muszę wypełnić tablicę IAsyncCall i wykonać AsyncMultiSync w plasterkach po 61.

Mój AsnycCalls Helper

Oto fragment TAsyncCallsHelper:

UWAGA: częściowy kod! (pełny kod dostępny do pobrania)
wykorzystuje AsyncCalls;
rodzaj
TIAsyncCallArray = tablica IAsyncCall;
TIAsyncCallArrays = tablica TIAsyncCallArray;
TAsyncCallsHelper = klasa
prywatny
fTasks: TIAsyncCallArrays;
własność Zadania: TIAsyncCallArrays czytać fZadania;
publiczny
procedura Dodaj zadanie(const call: IAsyncCall);
procedura WaitAll;
koniec;
UWAGA: częściowy kod!
procedura TAsyncCallsHelper. WaitAll;
var
i: liczba całkowita;
zaczynać
dla i: = Wysoki (zadania) aż do Niski (zadania) robić
zaczynać
AsyncCalls. AsyncMultiSync (zadania [i]);
koniec;
koniec;

W ten sposób mogę „czekać wszystko” w porcjach po 61 (MAXIMUM_ASYNC_WAIT_OBJECTS) - tj. Czekając na tablice IAsyncCall.

W związku z powyższym mój główny kod do zasilania puli wątków wygląda następująco:

procedura TAsyncCallsForm.btnAddTasksClick (Sender: TObject);
const
nrItems = 200;
var
i: liczba całkowita;
zaczynać
asyncHelper. MaxThreads: = 2 * System. CPUCount;
ClearLog („uruchamianie”);
dla i: = 1 do nrItems robić
zaczynać
asyncHelper. AddTask (TAsyncCalls. Invoke (AsyncMethod, i, Random (500)));
koniec;
Log („wszystko w”);
// czekaj wszystko
//asyncHelper.WaitAll;
// lub zezwól na anulowanie wszystkich nie rozpoczętych przez kliknięcie przycisku „Anuluj wszystko”:

podczas gdy nie asyncHelper. Wszystko skończone robić Podanie. ProcessMessages;
Dziennik („gotowy”);
koniec;

Anulować całość? - Musisz zmienić AsyncCalls.pas :(

Chciałbym również mieć sposób na „anulowanie” zadań, które są w puli, ale czekają na ich wykonanie.

Niestety plik AsyncCalls.pas nie zapewnia prostego sposobu anulowania zadania po dodaniu go do puli wątków. Nie ma IAsyncCall. Anuluj lub IAsyncCall. DontDoIfNotAlreadyExecuting lub IAsyncCall. Nie przejmuj się mną.

Aby to zadziałało, musiałem zmienić plik AsyncCalls.pas, próbując zmienić go w jak najmniejszym stopniu - tak że kiedy Andy wyda nową wersję, muszę dodać tylko kilka wierszy, aby uzyskać pomysł „Anuluj zadanie” pracujący.

Oto, co zrobiłem: dodałem „procedurę Anuluj” do IAsyncCall. Procedura Anuluj ustawia pole „FCancelled” (dodane), które jest sprawdzane, gdy pula ma rozpocząć wykonywanie zadania. Musiałem nieznacznie zmienić IAsyncCall. Zakończone (tak, że raporty połączeń zakończone nawet po anulowaniu) i TAsyncCall. Procedura InternExecuteAsyncCall (nie wykonywać połączenia, jeśli zostało anulowane).

Możesz użyć WinMerge aby łatwo zlokalizować różnice między oryginalnym plikiem asynccall.pas Andy a moją zmienioną wersją (zawartą w pliku do pobrania).

Możesz pobrać pełny kod źródłowy i eksplorować.

Wyznanie

OGŁOSZENIE! :)

The CancelInvocation Metoda zatrzymuje wywoływanie AsyncCall. Jeśli AsyncCall jest już przetworzony, wywołanie CancelInvocation nie działa, a funkcja Cancelled zwróci False, ponieważ AsyncCall nie został anulowany.
The Anulowany Metoda zwraca True, jeśli AsyncCall zostało anulowane przez CancelInvocation.
The Zapomnieć Metoda odłącza interfejs IAsyncCall od wewnętrznego AsyncCall. Oznacza to, że jeśli ostatnie odniesienie do interfejsu IAsyncCall zniknie, wywołanie asynchroniczne będzie nadal wykonywane. Metody interfejsu zgłoszą wyjątek, jeśli zostaną wywołane po wywołaniu Forget. Funkcja asynchroniczna nie może wywoływać w głównym wątku, ponieważ można ją wykonać po TThread. Mechanizm synchronizacji / kolejki został wyłączony przez RTL, co może spowodować martwy blokadę.

Pamiętaj jednak, że nadal możesz korzystać z mojego AsyncCallsHelper, jeśli musisz poczekać, aż wszystkie wywołania asynchroniczne zakończą się z „asyncHelper. WaitAll "; lub jeśli potrzebujesz „CancelAll”.