R05-04.DOC

(599 KB) Pobierz
Szablon dla tlumaczy

Rozdział 5

Aplikacje wielowątkowe

Jedną z najważniejszych cech 32-bitowej platformy Windows jest obsługa aplikacji wielowątkowych. Umożliwia wykorzystanie wszelkich zalet programowania współbieżnego, upraszcza proces programowania i generalnie czyni aplikacje łatwiejszymi w obsłudze. W 16-bitowych wersjach Windows nie było wielowątkowości, dlatego jest ona jednym z głównych czynników przemawiających za przenoszeniem aplikacji z Delphi 1 do wyższych, 32-bitowych wersji. W niniejszym rozdziale opiszemy mechanizmy Win32 API służące do realizacji aplikacji wielowątkowych oraz elementy Delphi stanowiące odzwierciedlenie tych mechanizmów; przy okazji przedstawimy ograniczenia związane z programowaniem współbieżnym w Delphi i postaramy się uzasadnić ich przyczyny.

Natura wątków

Wątek (thread) jest obiektem syste­mu operacyjnego, reprezentującym wydzieloną część kodu w ramach procesu. Każda aplikacja Win32 posiada przynajmniej jeden wątek zwany wątkiem głównym albo wątkiem pierwotnym (primary thread, default thread); aplikacja może także posiadać inne wątki, zwane wątkami pobocznymi lub drugorzędnymi (secondary threads).

Mechanizm wątków pozwala na niezależną, jednoczesną realizację wielu różnych funk­cji aplikacji; jednoczesność ta jednak jest pozorna, gdyż w rzeczywistości polega to na szybkim przełączaniu procesora między poszczególnymi wątkami — na tyle szybkim, iż sprawia wrażenie realizacji jednoczesnej (chyba że komputer wyposażony jest w kilka procesorów, ale to już zupełnie inna sprawa).

Wskazówka

Wielowątkowość jest cechą środowiska 32-bitowego — nie istnieje ona (i nigdy nie będzie istnieć) w 16-bitowych wersjach Windows. Wielowątkowe aplikacje tworzone w Delphi nigdy nie będą więc kompatybilne z Delphi 1.

Rodzaje wielozadaniowości

Wielozadaniowość z wykorzystaniem wątków jest czymś zgoła innym niż wielozada­niowość (a właściwie jej namiastka) w 16-bitowym środowisku Windows 3.x. W ramach Windows 3.x możliwe jest jednoczesne uruchamianie wielu aplikacji, trudno jednak mówić o całkowitym ich podporządkowaniu systemowi operacyjnemu. Aplikacja, otrzy­mawszy od systemu sterowanie, zyskuje tym samym kontrolę nad czasem procesora i mo­że go zawłaszczyć do woli; takie zawłaszczenie — rozmyślne lub niezamierzone, np. na skutek zapętlenia, zawsze paraliżuje pracę systemu, a często prowadzi do jego załama­nia. Od aplikacji 16-bitowej wymaga się więc przestrzegania pewnych zasad współpracy z innymi aplikacjami; z tego względu wielozadaniowość Windows 3.x została nazwana wielozadaniowością kooperacyjną (cooperative multitasking).

W Win32 wielozadaniowość ma całkowicie odmienny charakter. Obiektami ubiegającymi się o czas procesora są nie zadania, lecz właśnie wątki, nie to jest jednak najważniejsze: znacznie istotniejsza jest niemożność zmonopolizowania czasu procesora przez pojedynczy wątek. Otrzymuje on jedynie kwant czasu, po wykorzystaniu którego jest po prostu wywłaszczany (bez ostrzeżenia) przez system operacyjny. Mamy więc do czynienia z sytuacją, kiedy to system operacyjny ustala reguły gry, przydzielając czas poszczególnym wątkom i odbierając im sterowanie, gdy uzna to za stosowne; tego typu wielozadaniowość została nazwana wielozadaniowością z wywłaszczaniem (preemptive multitasking).

Do czego może się przydać wielowątkowość?

Możliwość podziału aplikacji na niezależne wątki jest dla programisty (nie tylko w Win­dows) niezwykle atrakcyjna, i to z wielu względów. Zalety wielowątkowości stają się szczególnie widoczne w przypadku, gdy aplikacja wykonuje jedną lub kilka akcji „w tle”, niezależnie od dialogu, który jednocześnie prowadzi z użytkownikiem w ramach swego interfejsu. Dobrym tego przykładem może być obliczanie wartości komórek arkusza kal­kulacyjnego równolegle z wprowadzaniem nowych danych lub — coraz powszechniej­sze — drukowanie wyników aplikacji równolegle z innymi jej działaniami. Projektant aplikacji może się skupić na dialogu z użytkownikiem uznając, że cała reszta zostanie „załatwiona” w ramach innych wątków. Zresztą — tak bardzo pożądana w procesie pro­jektowania — metoda dekompozycji problemów daje się bardzo łatwo zrealizować właśnie dzięki wielowątkowości; można więc powierzyć poszczególne aspekty aplikacji poszcze­gólnym jej wątkom, opracowywanym niezależnie od siebie, z uwzględnieniem jedynie niezbędnej synchronizacji (o czym będziemy pisać w dalszej części rozdziału).

 

Wielowątkowość a komponenty VCL

Tak się jednak składa, iż większa część biblioteki VCL nie jest „bezpieczna wątkowo” (thread-safe) — przy jej tworzeniu przyjęto bowiem założenie, iż w danej chwili dostęp do komponentów ma co najwyżej jeden wątek. Ograniczenie to dotyczy w większości komponentów tworzących interfejs użytkownika, chociaż wiele innych komponentów także nie jest przystosowanych do dostępu wielowątkowego. Dla niektórych z nich VCL udostępnia „wielowątkowe alternatywy” — na przykład TThreadList jest bezpieczną wątkowo odmianą komponentu TList. Przykładem mechanizmu przystosowanego do wielowątkowości jest natomiast strumieniowanie komponentów — dopuszcza się odczyt (lub zapis) strumieni (np. plików .DFM) jednocześnie przez kilka wątków.

W stosunku do komponentów tworzących interfejs użytkownika obowiązuje w VCL zastrzeżenie, iż ich obsługa może się odbywać jedynie w kontekście wątku głównego aplikacji — ważnym wyjątkiem od tej zasady jest obiekt płótna (Canvas), który posiada wbudowane mechanizmy obsługi wielowątkowej. Nie oznacza to oczywiście całkowitego odizolowania wątków pobocznych od komponentów, ponieważ Delphi udostępnia narzędzia umożliwiające modyfikowanie interfejsu użytkownika w kontekście wątku głównego, lecz z inicjatywy wątków pobocznych. Nie zmienia to jednak faktu, iż w warunkach aplikacji wielowątkowej obsługa interfejsu użytkownika musi być zrealizowana szczególnie starannie.

Błędne wykorzystanie wielowątkowości

Nadmiar dobrego czasami przeobraża się w zło; ta zasada ma zastosowanie również w odniesieniu do wąt­ków Win32 API. Choć podział aplikacji na niezależne wątki uwalnia programistę od wielu problemów, to jednocześnie przysparza mu wielu nowych kłopotów — tyle że innego rodzaju.

Przede wszystkim krytyczny staje się problem synchronizacji dwóch lub kilku wątków wykorzystujących te same zasoby. Wyobraź sobie wprowadzanie zmian do tekstu pro­gramu, który właśnie jest kompilowany: jeśli kompilator i edytor nie są nawzajem świa­dome swoich skutków, to taka sytuacja przypomina przestawienie zwrotnicy pod prze­jeżdżającym pociągiem. W tym szczególnym przypadku środki zaradcze są niemal banalne: można na przykład zablokować możliwość zmian w module na czas jego kom­pilacji, można też utworzyć kopię modułu i potraktować ją jako wejście dla kompilatora (jednak na czas kopiowania też trzeba zablokować edycję), można wreszcie śledzić po­stęp kompilacji (w przypadku kompilatorów jednoprzebiegowych) i umożliwić edycję tylko tej części tekstu, która została już skompilowana. Konkretne rozwiązanie nie jest tu istotne, ważne jest, aby nie traktować wielowątkowości jako panaceum na dotychczasowe problemy towarzyszące klasycznemu programowaniu sekwencyjne­mu. Programowanie współbieżne, oferując ogromne możliwości i rozwiązując proble­my, których rozwiązanie w ramach dotychczasowych środków mogło być jedynie poło­wiczne (lub żadne — bywa i tak), kryje jednocześnie wiele zdradliwych pułapek, gdy korzystamy z niego w niewłaściwy sposób.

Klasa TThread

Podstawową klasą Delphi, implementującą mechanizmy charakterystyczne dla wątków, jest klasa TThread. Chociaż jej właściwości i metody uwzględniają większość aspek­tów wielowątkowości (również tych specyficznych dla Delphi), to jednak w wielu wypad­kach (jak później zobaczymy) konieczne stają się bezpośrednie odwołania do Win32 API: najbardziej oczywistym tego przykładem są mechanizmy synchronizacji wątków, o której przed chwilą wspominaliśmy. Obecnie skoncentrujmy się jednak na samej klasie TThread; jej deklaracja, prezentowana poniżej, znajduje się w module Classes.pas.

 

  TThread = class

  private

    FHandle: THandle;

{$IFDEF MSWINDOWS}

    FThreadID: THandle;

{$ENDIF}

{$IFDEF LINUX}

    // ** FThreadID is not THandle in Linux **

    FThreadID: Cardinal;

    FCreateSuspendedSem: TSemaphore;

    FInitialSuspendDone: Boolean;

{$ENDIF}

    FCreateSuspended: Boolean;

    FTerminated: Boolean;

    FSuspended: Boolean;

    FFreeOnTerminate: Boolean;

    FFinished: Boolean;

    FReturnValue: Integer;

    FOnTerminate: TNotifyEvent;

    FMethod: TThreadMethod;

    FSynchronizeException: TObject;

    FFatalException: TObject;

    procedure CheckThreadError(ErrCode: Integer); overload;

    procedure CheckThreadError(Success: Boolean); overload;

    procedure CallOnTerminate;

{$IFDEF MSWINDOWS}

    function GetPriority: TThreadPriority;

    procedure SetPriority(Value: TThreadPriority);

    procedure SetSuspended(Value: Boolean);

{$ENDIF}

{$IFDEF LINUX}

    // ** Priority is an Integer value in Linux

    function GetPriority: Integer;

    procedure SetPriority(Value: Integer);

    function GetPolicy: Integer;

    procedure SetPolicy(Value: Integer);

    procedure SetSuspended(Value: Boolean);

{$ENDIF}

  protected

    procedure DoTerminate; virtual;

    procedure Execute; virtual; abstract;

    procedure Synchronize(Method: TThreadMethod);

    property ReturnValue: Integer read FReturnValue write FReturnValue;

    property Terminated: Boolean read FTerminated;

  public

    constructor Create(CreateSuspended: Boolean);

    destructor Destroy; override;

    procedure AfterConstruction; override;

    procedure Resume;

    procedure Suspend;

    procedure Terminate;

    function WaitFor: LongWord;

    property FatalException: TObject read FFatalException;

    property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;

    property Handle: THandle read FHandle;

{$IFDEF MSWINDOWS}

    property Priority: TThreadPriority read GetPriority write SetPriority;

{$ENDIF}

{$IFDEF LINUX}

    // ** Priority is an Integer **

    property Priority: Integer read GetPriority write SetPriority;

    property Policy: Integer read GetPolicy write SetPolicy;

{$ENDIF}

    property Suspended: Boolean read FSuspended write SetSuspended;

{$IFDEF MSWINDOWS}

    property ThreadID: THandle read FThreadID;

{$ENDIF}

{$IFDEF LINUX}

    // ** ThreadId is Cardinal **

    property ThreadID: Cardinal read FThreadID;

{$ENDIF}

    property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;

  end;

 

Jak widać, klasa TThread jest bezpośrednim potomkiem klasy TObject, więc obiekt klasy TThread nie jest komponentem i nie znajdziemy go w palecie komponentów. Liczne dyrektywy $IFDEF w deklaracji klasy świadczą o tym, iż jest ona klasą uniwersalną w sensie zgodności z Delphi i z Kyliksem. Na uwagę zasługuje także fakt, iż metoda Execute(), realizująca wątek w sensie fizycznym, jest metodą abstrakcyjną; oznacza to, iż abstrakcyjna jest cała klasa TThread, a więc w konkretnej aplikacji musimy posługiwać się jej klasami pochodnymi, przedefiniowującymi metodę Execute() stosownie do specyfiki poszczególnych wątków.

Najprostszym sposobem utworzenia nowej klasy wątku jest wybranie pozycji Thread Object z karty New okna New Items (rys. 5.1):

 

Rysunek 5.1. Definiowanie nowego wątku za pomocą repozytorium

 

Po wybraniu obiektu Thread Object Delphi wyświetli pytanie o nazwę tworzonej klasy; przyjmijmy, iż jest nią TTestThread. Po wprowadzeniu nazwy Delphi utworzy nowy moduł zawierający deklarację nowej klasy z przedefiniowaną metodą Execute():

 

type

  TTestThread = class(TThread)

  private

    { Private declarations }

  protected

    procedure Execute; override;

  end;

 

Nie siląc się w tym momencie na jakiś wyrafinowany przykład, uczyńmy treścią tej metody jakieś proste obliczenia, na przykład takie:

procedure TTestThread.Execute;

var

  k: integer;

 

begin

  for k := 1 to 2000000 do

    Inc( Answer, Round(Abs(Sin(Sqrt(k)))));

end;

 

Umieśćmy teraz na formularzu przycisk, którego kliknięcie spowoduje utworzenie obiektu zdefiniowanej klasy wątku:

 

procedure TForm1.Button1Click(Sender: TObject);

var

  NewThread: TTestThread;

begin

  NewThread := TTestThread.Create(False);

end;

Pojedynczy parametr wywołania konstruktora klasy wątkowej określa sposób postępowania z utworzonym obiektem wątku; jeżeli ma wartość False, wątek jest automatycznie uruchamiany, w przeciwnym razie wątek ten pozostaje w stanie zawieszenia — jego uruchomienie nastąpi dopiero w wyniku wywołania metody Resume(). Ta druga możliwość daje okazję do zmodyfikowania niektórych właściwości obiektu wątkowego przed jego uruchomieniem. Modyfikowanie działającego wątku jest w wielu przypadkach nieskuteczne, często też daje efekty różne od zamierzonych.

Możliwość wstrzymywania zawieszonego wątku nie jest cechą Delphi, lecz Win32; wstrzymanie takie następuje wówczas, gdy tworząca nowy wątek funkcja CreateThread() wywołana zostaje z parametrem CREATE_SUSPENDED.

W procedurze TForm1.Button1Click parametr wywołania konstruktora ma wartość False, zatem tworzony wątek jest automatycznie uruchamiany. Łatwo się wówczas przekonać, iż funkcjonowanie wątku pobocznego w niczym nie blokuje możliwości manipulowania formularzem — jego przemieszczania, minimalizacji, maksymalizacji, zmiany rozmiarów itp.

Obiekty wątków a zmienne

Przyjrzyjmy się zmiennej lokalnej k w procedurze TTestThread.Execute() i zastanówmy się, co się stanie w przypadku równoległej pracy kilku egzemplarzy wątku TTestThread: czy będą one wspólnie wykorzystywać tę zmienną, co, rzecz jasna, musiałoby doprowadzić do nieprzewidywalnych wyników? Czy może dostęp do niej bę­dzie się odbywał według jakichś priorytetów? Nic z tych rzeczy: każdy wątek posiada własny, oddzielny obszar stosu, a ponieważ zmienne lokalne umieszczane są właśnie na stosie, każdy wątek będzie się posługiwał własną, oddzielną kopią zmiennej k.

Jednak zupełnie inaczej rzecz się ma ze zmiennymi globalnymi; ich rozłączność musi być zapewniona za pomocą specjalnych środków, które opisze­my w dalszej części rozdziału.

Kończenie wątku

Zasadnicza akcja wątku reprezentowanego przez obiekt klasy wątkowej rozgrywa się w ramach metody Execute(), toteż jej zakończenie równoważne jest zakończeniu sa­mego wątku. Po zakończeniu wątku wywoływana jest funkcja Delphi o nazwie EndThread(), wywołująca z kolei funkcję API ExitThread() zwalniającą przydzielony do wątku stos i związany z wątkiem obiekt Win32.

Należy także zadbać o zwolnienie obiektu klasy wątkowej w Delphi. Zwróć uwagę, iż zwykłe w...

Zgłoś jeśli naruszono regulamin