R_6-04.doc

(298 KB) Pobierz
Szablon dla tlumaczy

 

Rozdział 6                                                                                                                                   Aplikacje wielowątkowe

             

              Możliwość tworzenia aplikacji wielowątkowych posługujących się zaletami programowania współbieżnego jest jedną z najbardziej atrakcyjnych technik, oferowanych w Windows przez 32-bitowy interfejs programisty. Teoretycznie rzecz biorąc, każdej części pisanego kodu można przyporządkować oddzielny wątek (ang. thread), stanowiący pewien obiekt wykorzystywany przez system operacyjny w ramach danego procesu. Każda projektowana przez nas aplikacja ma co najmniej jeden wątek główny, w którym możemy tworzyć kolejne, zwane wątkami drugorzędnymi. Wielowątkowość nierozerwalnie wiąże się z pojęciem wielozadaniowości. W Win32 API to właśnie wątki są tymi obiektami, które mogą ubiegać się o czas procesora. Nie ma wówczas możliwości całkowitego podporządkowania pracy procesora pojedynczemu wątkowi. System operacyjny sam decyduje, jaki czas należy przydzielić poszczególnym wątkom, po upływie którego mogą zostać wywłaszczone. Niekiedy nazywamy to wielozadaniowością z wywłaszczeniem.

O naturze wątków napisano już bardzo wiele, wystarczy wymienić znakomite książki: „Delphi 3. Księga eksperta”, wyd. Helion (1998) czy „Delphi 4. Vademecum profesjonalisty. Tom 1”, wyd. Helion (1999). W rozdziale tym nie będzie nas jednak interesować cała, niezwykle szeroka oferta programistyczna udostępniana przez wielowątkowość. Chociaż technika programowania współbieżnego oferuje nam olbrzymie możliwości, ma jednak i drugą stronę. Niewłaściwe je użycie może się okazać katastrofalne w skutkach dla działającej aplikacji. Trzeba zdawać sobie sprawę, że pisane przez nas programy są tworami dosyć specyficznymi. Technika ich projektowania znacznie odbiega od sposobu tworzenia stron WWW, skomplikowanych arkuszy kalkulacyjnych lub baz danych. O ile w przypadku wymienionych aplikacji po drugiej stronie jest zawsze inny człowiek (odbiorca), który bywa czasami wyrozumiały na popełnione przez nas niewielkie błędy, to projektując program sterujący jakimś urządzeniem, tego komfortu już nie mamy. Testy takich programów są zawsze bezlitosne, a ich ocena wyraża się prostą logiką zero-jedynkową (FALSE or TRUE). Gdy nieopatrznie wpiszemy jakąś komórkę w arkuszu kalkulacyjnym, błąd taki możemy naprawić stosunkowo prosto. Co się natomiast stanie, gdy do zasilacza wysokiego napięcia podłączonego do jakiegoś przyrządu wyślemy komendę: :VOLTage 550 zamiast prawidłowej :VOLTage 250? Różnica niby nieznaczna, tylko jedna cyfra... ale niekiedy po urządzeniu może pozostać jedynie wspomnienie, zaś na naszym koncie spory debet. Dlatego dalej skoncentrujemy się na pewnych podstawowych, ale skutecznych metodach posługiwania się techniką programowania współbieżnego z perspektywą użytecznego, a zarazem bezpiecznego jej wykorzystania w aplikacjach realizujących szeregową transmisję danych poprzez interfejs RS 232C.              

Najważniejszy jest Użytkownik

 

              Projektując każdą aplikację, musimy pamiętać, że oprócz poprawnego działania musi charakteryzować się jeszcze kilkoma bardzo ważnymi cechami. Dokładny opis podstawowych reguł dotyczących sposobu tworzenia programów o różnorodnym przeznaczeniu był tematem wielu publikacji, dlatego w tym miejscu przedstawię tylko najważniejsze spośród nich, mające zarazem bezpośrednie odniesienie to tego, co już stworzyliśmy. Wielowątkowe działanie naszych programów uwzględnimy, dostosowując je do nowych wymagań, jakie przed nimi zostaną postawione.   

Użytkownik steruje programem

 

              Dla większości z nas irytującą bywa sytuacja gdy podczas pracy z jakąś aplikacją uświadomimy sobie, że w pewnym momencie kontrolę nad nami zaczął sprawować komputer. Testując przedstawione do tej pory programy realizujące transmisję szeregową na pewno nie raz mieliśmy takie odczucie. Najbardziej widoczne jest to w przypadku programów transmitujących i odbierających pliki. Gdy zaczęliśmy transmisję wybranego wcześniej pliku praktycznie nie można było już nic zrobić. Aplikacja, pozostając nieruchomą na ekranie, nie reagowała na próbę naciśnięcia jakiegokolwiek przycisku aż do momentu zakończenia danego zadania. Pierwszym określeniem, jakie przychodzi mi wówczas na myśl, jest bezwładność i pewna ociężałość takiego produktu. Niewielu Użytkownikom podobają się tak działające aplikacje. Wyzwaniem będzie wówczas dla programisty wymyślenie sposobu, dzięki któremu program stanie się bardziej przyjazny wobec otoczenia.

Możliwość anulowania decyzji             

 

              Uwzględnienie tej opcji wynika bezpośrednio z poprzedniego punktu. Poprawnie zaprojektowane programy komunikacyjne powinny przynajmniej częściowo umożliwiać Użytkownikowi anulowanie, nawet w czasie transmisji niektórych podjętych wcześniej decyzji. Ponownie posłużmy się przykładem aplikacji transmitującej plik. Pamiętamy, że jednym ze sposobów zabezpieczenia się przed wysłaniem niechcianego zbioru danych jest obejrzenie go tuż przed transmisją w jednym z okien edycyjnych. Niestety, konstrukcja dotychczasowych programów umożliwiała nam jedynie obejrzenie pliku, nie mieliśmy natomiast żadnej możliwości przerwania w dowolnym momencie jego transmisji i ewentualnie wznowienia jej, nie ingerując zbytnio w tempo działania aplikacji. Ten punkt odnosi się głównie do problemu transmisji większych pakietów danych, gdyż trudno wyobrazić sobie sytuację anulowania decyzji w trakcie wysyłania jednego znaku.   

Możliwość odbioru komunikatu nawet w trakcie wysyłania danych

 

              Uwzględnienie tej opcji w naszych aplikacjach może wydać się nieco dziwne. Każdy  Czytelnik zdaje sobie oczywiście sprawę z pewnych ograniczeń, jakie nakłada na nas sam fakt posługiwania się szeregową transmisją asynchroniczną. Wykorzystanie w naszych algorytmach tej własności wcale nie będzie wymagało zastosowania jakiegoś wyszukanego okablowania czy niezrozumiałej modyfikacji protokołu transmisji. Zupełnie wystarczy, jeżeli w pełni wykorzystamy poznane już zalety, związane z podwójnym buforowaniem danych. 

Możliwość wysłania odrębnej informacji w trakcie transmisji pliku

 

              Posługując się różnego rodzaju programami nadzorującymi proces transmisji szeregowej, możemy spotkać się z koniecznością wysłania jakiejś wiadomości (niekoniecznie bardzo krótkiej) w trakcie transmisji dłuższej porcji informacji. W takich sytuacjach musimy uwzględnić fakt zachwiania parytetu kolejki znaków w buforze wyjściowym. Wcześniej została omówiona funkcja TransmitCommChar(), jednak stosowanie jej bywa nieco uciążliwe, głównie z tego powodu, że argumentem jej może być tylko jeden znak. Naprawdę funkcjonalna aplikacja do transmisji szeregowej powinna posiadać opcję, pozwalającą na szybką modyfikację kolejki znaków będących już w buforze wyjściowym, jednak bez naruszenia ich fizycznej spójności.

 

 

              Być może postulaty dotyczące spodziewanego rozwoju pisanych do tej pory programów zawarte w powyższych punktach nieco zaniepokoiły niektóre osoby. Można by odnieść ze wszech miar błędne wrażenie, że to, co zrobiliśmy do tej pory, zostanie poddane jakiejś strasznie skomplikowanej modyfikacji w celu dostosowania stworzonych już i poprawnie działających przecież aplikacji do tych nowych warunków. Ktoś mógłby się spodziewać, że oto czeka nas żmudny proces poznawania wielu kolejnych funkcji Win32 API, struktur czy typów danych, nie mówiąc już o konieczności zapoznania się ze specyficznymi własnościami Delphi czy C++Buildera. Już w tym miejscu mogę obiecać, że choć nie unikniemy tego całkowicie, to jednak zrobię to w formie jak najbardziej przystępnej. Wszystkie zaprojektowane do tej pory aplikacje zachowają swój oryginalny kształt. Uwzględnimy możliwość ich pracy wielowątkowej, uwzględnimy tylko nieznacznie je wzbogacając.   

              Pamiętając o wszystkim, czego dokonaliśmy do tej pory oraz mając na uwadze przedstawione nowe zadania, stojące przed naszymi programistycznymi produktami, nie pozostaje nam już nic innego, jak tylko uzupełnić stworzone już aplikacje o możliwość ich pracy wielowątkowej. Po przeczytaniu sporego fragmentu tej książki osoby preferujące Delphi mogły poczuć się nieco zawiedzione tym, że zawsze nowy temat rozpoczynałem do przykładów pisanych w C++Builderze. Aby im to wynagrodzić, tym razem zaczniemy od Object Pascala.

Delphi

 

Jak zapewne wiemy, istnieje w Delphi pewna klasa służąca implementacji mechanizmów, którymi charakteryzują się wątki. Jest nią TThread. Korzystając z jej właściwości oraz metod, z powodzeniem uwzględnić można bardzo wiele aspektów wielowątkowości. W celu utworzenia nowego wątku można posłużyć się niezwykle ciekawymi właściwościami parametrów konstruktora TThread.Create(), którego definicję przytoczę za Borland Delphi Visual Component Library:

 

constructor TThread.Create(CreateSuspended: Boolean);

var

  Flags: DWORD;

begin

  inherited Create;

  AddThread;

  FSuspended := CreateSuspended;

  Flags := 0;

  if CreateSuspended then Flags := CREATE_SUSPENDED;

  FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), Flags,

                         FThreadID);

end;

 

Przedstawiony konstruktor dokonuje wywołania funkcji BeginThread() zdefiniowanej w Borland Delphi Run-Time Library Win32 API Interface Unit:

 

function BeginThread(SecurityAttributes: Pointer; StackSize: LongWord;

                     ThreadFunc: TThreadFunc; Parameter: Pointer;

                     CreationFlags: LongWord; var ThreadId: LongWord):

                     Integer;

var

  P: PThreadRec;

begin

  New(P);

  P.Func := ThreadFunc;

  P.Parameter := Parameter;

  IsMultiThread := TRUE;

  Result := CreateThread(SecurityAttributes, StackSize,

              @ThreadWrapper, P, CreationFlags, ThreadID);

end;

 

Jak zauważyliśmy, funkcja BeginThread() dokonuje z kolei wywołania kolejnej o nazwie CreateThread(), tworząc tym samym nowy wątek. Należy jednak pamiętać, że samo utworzenie wątku wcale nie musi oznaczać jego automatycznego uruchomienia. Jeżeli w konstruktorze TThread.Create() jako wartość parametru CreateSuspended obierzemy FALSE (0), wątek zostanie natychmiast uruchomiony. W przeciwnym wypadku (TRUE lub 1) funkcja CreateThread() zostanie wywołana z parametrem CREATE_SUSPENDED, powodując, że działanie nowo utworzonego wątku będzie zawieszone do czasu wywołania metody TThread.Resume:

 

procedure TThread.Resume;

begin

  if ResumeThread(FHandle) = 1 then FSuspended := FALSE;

end;

 

Powtórne zawieszenie działania wątku nastąpi oczywiście po wywołaniu: 

 

procedure TThread.Suspend;

begin

  FSuspended := True;

  SuspendThread(FHandle);

end;

 

Sam proces realizacji wątku odbywa się w ramach metody Execute, wywoływanej w funkcji :

 

function ThreadProc(Thread: TThread): Integer;

var

  FreeThread: Boolean;

begin

  Thread.Execute;

  FreeThread := Thread.FFreeOnTerminate;

  Result := Thread.FReturnValue;

  Thread.FFinished := True;

  Thread.DoTerminate;

  if FreeThread then Thread.Free;

  EndThread(Result);

end;

 

Widzimy, że zakończenie wątku nastąpi dzięki wywołaniu procedury EndThread() z parametrem, którego wartość równa się właściwości FReturnValue (w omawianej klasie domyślnie przyjmowane jest 0), będącej zarazem kodem zakończenia danego wątku. Kod ten można odczytać, wykorzystując w tym celu funkcję GetExitCodeThread(). 

Dla porównania prześledźmy jeden z możliwych sposobów tworzenia nowego wątku przy wykorzystaniu niektórych funkcji Win32 API. Postępując zgodnie z ideą poprzedniego rozdziału, skoncentrujemy się na jednej z metod bezpośredniego odwołania do Win32 API, gdzie zdefiniowana jest funkcja CreateThread(), za pomocą której można utworzyć i uruchomić nowy wątek w obrębie przestrzeni adresowej odpowiedniego procesu. Funkcja ta zwraca identyfikator nowo utworzonego wątku.

             

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,               

                    DWORD dwStackSize,

                    LPTHREAD_START_ROUTINE lpStartAddress,             

                    LPVOID lpParameter, DWORD dwCreationFlags,

                    LPDWORD lpThreadId);

 

 

 

W Object Pascalu skorzystamy z analogicznej definicji:

 

function CreateThread(SecurityAttributes: Pointer;

                      StackSize: LongWord; ThreadFunc: TThreadFunc;

                      Parameter: Pointer; CreationFlags: LongWord;

                      var ThreadId: LongWord): Integer; stdcall;

 

lpThreadAttributes jest wskaźnikiem do struktury SECURITY_ATTRIBUTES, określającej pewne atrybuty zabezpieczeń nowego wątku. Mam nadzieję, że nie będzie nikogo razić, jeżeli dalej przedstawię definicje właściwe zarówno Win32 API jak i Borland Delphi Run-Time Library Win32 API Interface Unit

 

typedef struct _SECURITY_ATTRIBUTES {  

    DWORD  nLength;

    LPVOID lpSecurityDescriptor;

    BOOL   bInheritHandle;

} SECURITY_ATTRIBUTES;

 

lub

 

PSecurityAttributes = ^TSecurityAttributes;

  {$EXTERNALSYM _SECURITY_ATTRIBUTES}

  _SECURITY_ATTRIBUTES = record

    nLength: DWORD;

    lpSecurityDescriptor: Pointer;

    bInheritHandle: BOOL;

  end;

  TSecurityAttributes = _SECURITY_ATTRIBUTES;

  {$EXTERNALSYM SECURITY_ATTRIBUTES}

  SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES;

 

W obu definicjach nLength jest rozmiarem struktury (rekordu). Przed przekazaniem tego rekordu (struktury) jako parametru w przypadku ogólnym należy wpisać do nLength wartość równą sizeof(SECURITY_ATTRIBUTES). lpSecurityDescriptor jest wskaźnikiem do deskryptora zabezpieczeń wątku jako obiektu. Jeżeli ustalimy NULL (w Pascalu NIL), obiektowi zostanie przydzielona wartość domyślna rodzaju zabezpieczeń w trakcie danego procesu, zaś identyfikator wątku nie będzie dziedziczony. Z kolei bInheritHandle specyfikuje, czy zwracany identyfikator utworzonego wątku jest dziedziczony przez nowy proces. Ustalenie jej jako TRUE zapewni, że ten identyfikator będzie mógł dziedziczyć każdy nowy proces.  

dwStackSize jest rozmiarem obszaru pamięci (w bajtach), zwanej stosem, z  której korzysta dany proces. Jeżeli przyjmiemy tu wartość 0, rozmiar stosu dla nowego wątku będzie taki sam jak dla wątku głównego. Stos jest automatycznie alokowany w pamięci procesu i automatycznie zwalniany po wstrzymaniu działania wątku. Jeżeli deklarowany rozmiar stosu przewyższa ilość dostępnej pamięci, nie zostanie przydzielony identyfikator do nowego wątku.  

 

lpStartAddress stanowi wskaźnik do części aplikacji (lub funkcji) wykonywanej w danym wątku podając jednocześnie jej adres startowy. Funkcja może zawierać pojedynczy 32-bitowy argument, zwracając jednoczenie 32-bitową wartość.

 

lpParameter specyfikuje pojedynczą 32-bitową wartość parametru przekazywanego wątkowi.

dwCreationFlags podaje odpowiedni znacznik kontroli sposobu utworzenia nowego wątku. Jeżeli wyspecyfikujemy dobrze nam znany parametr CREATE_SUSPENDED, działanie wątku będzie zawieszone do czasu wywołania funkcji ResumeThread(). Jeżeli zostanie tu przypisana wartość 0, nowy wątek zostanie natychmiast uruchomiony. 

lpThreadId jest wskaźnikiem do 32-bitowej zmiennej identyfikującej wątek. Nie należy jednak w żadnym wypadku mylić jej z identyfikatorem nowego wątku.             

              Opisana funkcja tworzy i przypisuje identyfikator do nowo powstałego wątku. Jeżeli wskaźnik do struktury zabezpieczeń obiektu nie jest używany, identyfikator ten może być wykorzystany przez dowolną funkcję, której wywołanie wymaga użycia unikalnego identyfikatora wątku jako obiektu Win32 API. Uruchomienie wątku zaczyna się od wywołania funkcji specyfikowanej przez lpStartAddress. Aby zatrzymać działający wątek, należy odwołać się do funkcji Win32 API:

 

VOID ExitThread(DWORD dwExitCode);

 

lub zdefiniowanej w Borland Delphi Run-Time Library procedury:

 

procedure ExitThread(ExitCode: Integer); stdcall;

 

gdzie dwExitCode (ExitCode) specyfikuje kod zakończenia danego wątku. Użycie tej funkcji (procedury) jest preferowaną metodą opuszczania działającego wątku. Gdy jest ona wywoływana (obojętnie czy w sposób jawny, czy też w inny), obszar aktualnego stosu zostanie zwolniony, zawieszając tym samym działanie wątku. W celu otrzymania kodu ostatniego wątku należy posłużyć się funkcją:

 

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);

 

która w Borland Delphi Run-Time Library definiowana jest jako:

 

function GetExitCodeThread(hThread: THANDLE; var lpExitCode: DWORD):

                           BOOL; stdcall;

 

hThread jest identyfikatorem wątku, (w Win NT musi być  przydzielony z rodzajem dostępu THREAD_QUERY_INFORMATION), zaś lpExitCode jest wskaźnikiem do 32-bitowej zmiennej, reprezentującej kod zakończenia wątku.

             

              Technika wykorzystania klasy TThread oraz sposoby posługiwania się funkcją CreateThread(), są — jak być może zauważyliśmy — nieco skomplikowane i przyznam w tym miejscu, że mało przydatne do naszych celów. Przecież przyjęliśmy zasadę, że nie będziemy wiele zmieniać w konstrukcji dotychczasowych algorytmów. Istnieje dużo prostszy sposób implementacji wątków w programach sterujących transmisją szeregową. Powróćmy do prezentowanej już funkcji BeginThread(). Funkcję tę można z powodzeniem wykorzystać w aplikacjach pisanych zarówno w Delphi jak i C++Builderze. Umiejętne jej użycie zapewni nam uruchomienie osobnego wątku, bez potrzeby jawnego i bezpośredniego odwoływania się do funkcji Win32 API CreateThread() (co wcale nie oznacza, że znajomość jej jest bezużyteczna). Wielką zaletą posługiwania się BeginThread() jest fakt, że możemy w niej odwołać się do normalnej funkcji Pascala lub C++, która już dzięki temu będzie mogła być potraktowana jako osobny wątek. Rolę takiej funkcji z powodzeniem może pełnić typ:

 

TThreadFunc = function(Parameter: Pointer): Integer;

 

TThreadFunc definiuje pewien typ funkcji, która już w momencie użycia traktowana jest jako adres startowy nowego wątku (obiektu Win32). Może być on przekazywany bezpośrednio do  BeginThread() lub do funkcji Win32 API CreateThread(). 32-bitowy wskaźnik Parameter jest przekazywany bezpośrednio do BeginThread(). Dodam na marginesie, że dla naszych specyficznych celów stosowanie tego parametru nie jest wymogiem koniecznym wymogiem.

              Zobaczmy, jak praktycznie w bardzo prosty sposób można wprowadzić pewne elementy wielowątkowości do naszych aplikacji. Przede wszystkim należy skonstruować własną odmianę typu TThreadFunc. Nic prostszego — wystarczy odpowiednio wykorzystać na przykład procedurę ob...

Zgłoś jeśli naruszono regulamin