AdaVirtus

 
  • Increase font size
  • Default font size
  • Decrease font size

Rozdział 10: Bezpieczna wielozadaniowość

Spis treści

W świecie rzeczywistym wiele rzeczy dzieje się równocześnie. Człowiek potrafi łatwo robić wiele czynności równolegle. Wygląda na to, że kobietom przychodzi to o wiele łatwiej niż mężczyznom. Pewnie dlatego, że muszą bawić dziecko w czasie gotowania obiadu i wyganiać tygrysa z jaskini. Mężczyzna zazwyczaj koncentruje się na jednej czynności w danej chwili, na przykład polując na królika na obiad, lub szukając większej jaskini, albo nawet zajmując się wynajdywaniem koła.

Komputery tradycyjnie robią jedną rzecz w danej chwili, za to system operacyjny sprawia, że wydaje się nam, iż pewne działania przebiegają równolegle. Obecnie nie do końca jest to prawdą, ponieważ wiele komputerów ma wiele procesorów. Lecz prawda ta ma ciągle zastosowanie w przypadku małych komputerów, wliczając w to te, które są wykorzystywane w sterowaniu procesami.

10.1 Systemy operacyjne i zadania

Systemy operacyjne są ogromnie zróżnicowane, jeśli chodzi o wielkość zapewnianej współbieżności działań. Systemy operacyjne zgodne z POSIX dostarczają programiście wiele wątków sterowania. Wątki te mogą działać w programie całkiem niezależnie, czyli umożliwiają działania współbieżne.

W niektórych systemach jest jeden procesor, który jest zajmowany przez różne wątki w zależności od algorytmu szeregującego. Jedno z podejść to proste przydzielanie procesora dla każdego z wątków na krótki okres czasu. Bardziej wyszukane podejścia korzystają z priorytetów lub ostatecznych terminów celem zapewnienia efektywnego wykorzystania procesora.

Niektóre komputery mogą mieć kilka procesorów, w którym to przypadku wątki faktycznie działają równolegle. I ponownie, program szeregujący rezerwuje procesor - miejmy nadzieję, że efektywnie - dla aktywnych wątków sterowania.

W językach programowania równoległe działania zwane są ogólnie wątkami lub zadaniami. Drugi termin jest terminem wykorzystanym w Adzie. Języki mają bardzo różne podejście do zagadnienia wielozadaniowości. Niektóre mają wbudowane mechanizmy wielozadaniowości w samą definicję języka. Inne są wyposażone w prosty dostęp do właściwych funkcji systemowych systemu operacyjnego. Jeszcze inne ignorują ten temat całkowicie.

Ada i Java są językami z wbudowanymi mechanizmami wielozadaniowości. Języki C i C++ znów nie, przez co programiści używający tych języków muszą polegać na innych bibliotekach i wywołują usługi systemu operacyjnego.

Są co najmniej trzy zalety wielozadaniowości wbudowanej w język:

  • Wbudowana konstrukcje składniowe czynią pisanie prawidłowych program dużo łatwiejszym, gdyż język może uchronić przed wieloma błędami. Jest to właściwie historia "z brodą" o abstrakcji. Poprzez ukrycie szczegółów niskiego poziomu zapobiegamy pewnym błędom.
  • Przenośność jest utrudniona, jeśli są bezpośrednio wykorzystane usługi systemu operacyjnego, ponieważ panuje tutaj ogromne zróżnicowanie w podejściu do tematu.
  • Popularne systemy operacyjne nie zapewniają mechanizmów czasowych i innych udogodnień wymaganych przez aplikacje czasu rzeczywistego.

Typowe działania wymagane w programie wielozadaniowym to:

  • Zadania nie mogą powodować naruszenia integralności danych w sytuacji, gdy kilka zadań ma współbieżny dostęp do danych.
  • Zadania muszą się komunikować ze sobą celem transferu danych pomiędzy nimi.
  • Zadania muszą być kontrolowane pod kątem określonych wymagań czasowych.
  • Zadania muszą być szeregowane w sposób umożliwiający efektywną gospodarkę zasobami oraz aby spełnić wszystkie wymagania czasowe.

W rozdziale tym rzucimy okiem na te zagadnienia i pokażemy jak Ada je realizuje. To duże wyzwanie, gdyż dużo trudniej stworzyć prawidłowy program wielozadaniowy, niż zwykły program sekwencyjny. Na początek zaznajomimy się z prostą ideą zadania w Adzie, a później z całą strukturą programu.

Program adowy może mieć wiele zadań działających współbieżnie. Zadanie jest zapisane w dwóch częściach (podobnie jak pakiet). Ma ono swoją specyfikację, opisującą interfejs dostępny dla innych zadań, oraz treść, zawierającą kod, mówiący co zadanie robi. W prostych przypadkach specyfikacja po prostu nazywa zadanie:

task A;            -- specyfikacja zadania
 
task body A is     -- treść zadania
begin
   ...             -- instrukcje mówiące co zadanie robi
end A;

Czasami wygodnie jest mieć kilka podobnych zadań, w którym to przypadku wprowadzimy pojęcie typu zadaniowego:

task type Worker;
 
task body Worker is ...

Następnie możemy zadeklarować kilka zadań poprzez deklarację obiektów, jak zazwyczaj:

Tom, Dick, Harry: Worker;

Tworzymy w ten sposób trzy zadania nazwane Tom, Dick i Harry. Można również zadeklarować tablicę zadań lub też mieć składową rekordu w postaci zadania itd. Zadania mogą być deklarowane wszędzie tam, gdzie mogą być deklarowane normalne obiekty: w pakiecie, podprogramie, a nawet wewnątrz innego zadania. Nic dziwnego, że typy zadaniowe są typami ograniczonymi, gdyż przypisywanie jedngo zadania do drugiego nie jest zbyt sensownym działaniem.

Główny podprogram całego programu jest wywoływany przez tzw. zadanie środowiska, w którym to zadaniu przebiega proces opracowywania pakietów bibliotecznych (jak to opisano w rozdziale Bezpieczny start programu). Cały program wraz z pakietami bibliotecznymi A, B, C oraz podprogramem głównym może być przedstawiony w postaci:

task Environment_Task;
 
task body Environment_Task is
   ...         -- deklaracje pakietów bibliotecznych A, B, C
   ...         -- oraz podprogramu głównego Main
begin
   ...         -- wywołanie podprogramu głównego Main
end;

Zadanie staje się aktywne w momencie deklaracji. Kończy zaś swoje działanie przez osiągnięcie końca treści zadania. Ważną zasadą jest to, że zadanie lokalne zadeklarowane wewnątrz podprogramu lub innego zadania musi zakończyć swoje działanie przed zakończeniem działania jednostki zawierającej to zadanie. Jednostka ta będzie wstrzymana do momentu zakończenia wszystkich lokalnych zadań w niej zawartych. Reguła ta chroni przed wiszącymi odwołaniami do danych, które już nie istnieją.

10.2 Obiekty chronione

Przypuśćmy, że trzy zadania Tom, Dick i Harry korzystają ze stosu jako pewnego rodzaju tymczasowego urządzenia pamięciowego. Od czasu do czasu jedno z tych zadań umieszcza element na stosie i od czasu do czasu jedno z nich (może to samo, może inne) zdejmuje element ze stosu.

Trzy zadania działają współbieżnie, a działający system przydziela każdemu z nich procesor w oparciu o użyty algorytm. Możliwe, że każde zadanie dostaje procesor na 10 ms.

Przypuśćmy, że stos, którego używają powyższe zadania jest taki sam jak zadeklarowany w rozdziale Bezpieczna architektura. Załóżmy, że zadanie Harry wywołało procedurę Push w momencie, gdy jego czas już minął, a sterowanie zostaje przekazane do zadania Tom, które wywołuje procedurę Pop. Aby być dokładnym, przypuśćmy, że zadanie Harry utraciło procesor tuż po wykonaniu instrukcji zwiększającej wartość Top:

procedure Push (X: Float) is
begin
   Top := Top + 1;             -- zadanie Harry utraciło procesor tuż po wykonaniu tej instrukcji
   A (Top) := X;
end Push;

W tym momencie wartości Top jest zwiększona, ale nowa wartość X nie została przypisana do składowej tablicy. Gdy zadanie Tom wywołuje procedurę Pop otrzymuje starą i pewnie bezsensowną wartość ze składowej tablicy, która miała być nadpisana wartością nową. Gdy zadanie Harry odzyskuje procesor (i zakłada, że w międzyczasie nie było żadnych działań na stosie) zapisuje wartość X do składowej, która jako część stosu jest już nieużytkowana. Innymi słowy: tracimy wartość X.

Gorzej, jeśli procesor zostanie przełączony w trakcie wykonywania instrukcji. W taki sposób zadanie Harry może utracić dostęp do procesora tuż po pobraniu wartości Top do rejestru, ale przed zapisaniem nowej wartości do Top. Przypuśćmy, że w tym momencie zadanie Dick przejmuje kontrolę nad procesorem i wywołuje Push dodając 1 do starej wartości Top. Gdy zadanie Harry wraca do życia, zamienia wartość wyliczoną przez zadanie Dick na wartość taką samą. Inaczej mówiąc: dwa wywołania Push zwiększyły Top o 1, a nie - jak oczekiwaliśmy - o 2.

Takie niepożądane zachowanie jest przezwyciężone przez Adę poprzez użycie obiektu chronionego jako stosu:

protected Stack is
   procedure Clear;
   procedure Push (X: in Float);
   procedure Pop (X: out Float);
 
private
   Max: constant := 100;
   Top: Integer range 0 .. Max := 0;
   A: array (1 .. Max) of Float;
end Stack;
 
protected body Stack is
   procedure Clear is
   begin
      Top := 0;
   end Clear;
 
   procedure Push (X: in Float) is
   begin
      Top := Top + 1;
      A (Top) := X;
   end Push;
 
   procedure Pop (X: out Float) is
   begin
      X := A (Top);
      Top := Top – 1;
   end Pop;
end Stack;

Zwróćmy uwagę, że słowo package zostało zastąpione słowem protected, dane,które były w treści są teraz w części prywatnej nowej konstrukcji, a - z powodów niżej wyjaśnionych - funkcja Pop została zastąpiona procedurą Pop.

Trzy procedury Clear, Push i Pop są zwane operacjami chronionymi. Są one wywoływane w ten sam sposób jak procedury. Ich zachowanie jest takie, że tylko jedno zadanie ma w danym momencie dostęp do operacji obiektu. Jeśli zadanie takie jak Tom spróbuje wywołać procedurę Pop w momencie, gdy zadanie Harry wykonuje Push, to zadanie Tom zostaje wstrzymane do momentu, aż zadanie harty nie wróci z procedury Push. To wszystko dzieje się automatycznie bez jakiegokolwiek zaangażowania ze strony programisty. Dzięki temu wystrzegamy się wszelkich problemów związanych ze spójnością.

Patrząc za kulisy, obiekt chroniony posiada tzw. zatrzask. Zadanie, które próbuje dostępu do operacji obiektu wpierw musi pozyskać zatrzask. Jeśli inne zadanie pozyskało zatrzask wcześniej, to zadanie próbujące dostępu do operacji obiektu musi czekać do momentu aż dane zadanie zakończy operacje obiektu chronionego i ten zatrzask zwolni.

Powyższy przykład można zmodyfikować tak, aby pokazać jak sobie radzić z próbami umieszczania elementów na stosie, gdy ten jest już pełny. W ujęciu pakietowym przy próbie przypisania wartości Max+1 do Top nastąpi zgłoszenie wyjątku. W naszym przypadku mogłoby stać się tak samo, w następstwie czego automatycznie zwolniony by został zatrzask, ponieważ wyjątek kończy wywołanie procedury chronionej.

Można to jednak zrobić dużo lepiej. Musimy zmodyfikować obiekt chroniony tak, aby skorzystał z barier:

protected Stack is
   procedure Clear;
   entry Push (X: in Float);
   entry Pop (X: out Float);
 
private
   Max: constant := 100;
   Top: Integer range 0 .. Max := 0;
   A: array (1 .. Max) of Float;
end Stack;
 
protected body Stack is
   procedure Clear is
   begin
      Top := 0;
   end Clear;
 
   entry Push (X: in Float) when Top < Max is
   begin
      Top := Top + 1;
      A (Top) := X;
   end Push;
 
   entry Pop (X: out Float) when Top > 0 is
   begin
      X := A (Top);
      Top := Top – 1;
   end Pop;
end Stack;

Operacje Push i Pop zmieniły charakter: są teraz wejściami, a nie procedurami. Jako wejścia mają bariery w postaci wyrażeń logicznych takich jak np. Top<Max. Bariera działa w ten sposób, że jeśli jej wartość jest równa False, treść danego wejścia nie jest wykonywana (należy zwrócić uwagę na to, że wejście nadal jest wywoływane). Jedyne co się wydarzy, to wstrzymanie zadania do momentu, aż bariera przyjmie wartość True. Tak więc, jeśli zadanie Harry spróbuje wywołać Push, gdy stos jest pełny, będzie musiało czekać, aż jakieś inne zadanie (Tom lub Dick) nie wywoła Pop i nie zdejmie elementu ze stosu. Wtedy zadanie Harry automatycznie będzie kontynuowało swoje działanie. Użytkownik nie musi tutaj niczego specjalnie programować.

Warto zwrócić uwagę, że wejścia - podobnie jak procedury chronione - również wywoływane są w ten sam sposób, co procedury normalne:

Stack.Push (Z);

Podsumowując, mechanizm obiektów chronionych dostępny w Adzie dostarcza strukturalny mechanizm organizowania wzajemnie wykluczających się dostępów do współdzielonych danych. Obiekt chroniony deklaruje swoje operacje chronione (procedury, funkcje lub wejścia) w widzialnej części specyfikacji, a składowe chronione w części prywatnej. Treść obiektu chronionego zawiera implementację operacji chronionych. Procedury i wejścia chronione mają dostęp typu "odczyt/zapis" do składowych chronionych, tzn. mogą się do nich odwoływać i/lub dokonywać operacji przypisania. Funkcje - natomiast - mają możliwość tylko odczytu tych składowych. To ograniczenie pozwala na optymalizację w sytuacji, gdy wiele zadań może jednocześnie odczytywać dane chronione (poprzez wywołania funkcji chronionych). Jednak zapisywać dane chronione może tylko jedno zadanie w danym momencie (czasami mówimy o tym: współbieżny odczyt, wyłączny zapis). Zakaz przypisywania funkcji chronionych do chronionych składowych jest powodem, dla którego musieliśmy wyrazić operację Pop (w pierwszej wersji stosu jako obiektu chronionego) w postaci procedury, a nie funkcji.

Tak samo jak możemy deklarować typ zadaniowy jako szablon dla obiektów zadaniowych, tak samo możemy deklarować type chronione jako szablony obiektów chronionych. I podobnie jak zadania, typy chronione są typami ograniczonymi.

Bardzo pouczającym jest rozważenia jak można by było zaprogramować ten problem wykorzystując mechanizmy pierwotne niskiego poziomu. Historyczne mechanizmy tego typu to operacje P (opuść - proberen) oraz V (podnieś - verhogen) działające na obiektach zwanych semaforami. W rezultacie operacji P(sem) włączenie blokady powiązanej z sem, jeśli blokada jest dostępna. W przeciwnym wypadku zadanie wywołujące zostaje umieszczone w kolejce do momentu zwolnienia blokady. Wynikiem operacji V(sem) jest wyłączenie blokady związanej z sem i przebudzenie jednego z zadań (jeśli takowe jest) wstrzymanych w kolejce sem.

Pomysł jest taki, że umieszczamy parę P i V wokół operacji, dla których musimy zapewnić wzajemnie wykluczający się dostęp. Korzystając z Ady moglibyśmy zapisać Push w taki oto sposób:

procedure Push(X: in Float) is
begin
   P(Stack_Lock);      -- założenie blokady
   Top := Top + 1;
   A(Top) := X;
   V(Stack_Lock);      -- zwolnienie blokady
end Push;
 

Podobnie w przypadku Clear i Pop. Jest to właściwie metoda "zrób to sam" lub asemblerowy typ kodowania. Możliwości popełnienia błędów jest wiele:

  • można pominąć jedną z operacji (P lub V), co naruszy równowagę,
  • można zapomnieć otoczyć grupę instrukcji, która powinna być chroniona,
  • można użyć niewłaściwej nazwy semafora,
  • można niechcący pominąć operację zamykającą V.

Ostatni z problemów mógłby powstać, jeśli (w modelu bez barier) została wywołana operacja Push, gdy stos jest już pełny. To spowodowałoby zgłoszenie wyjątku Constraint_Error. Jeśli zaniechamy w lokalnej obsłudze wyjątków wywołania V, to system zostanie zablokowany na stałe.

Żadne z tych utrudnień nie powstaje, gdy wykorzystuje się obiekty chronione Ady, ponieważ wszystkie te operacje niskiego poziomu są wykonywane automatycznie. Chociaż semafory mogą być używane z powodzeniem w prostych sytuacjach, jest bardzo trudno zastosować je prawidłowo w bardziej skomplikowanych takich, jak przykład z barierami. Trudność nie polega tylko na samym programowaniu z użyciem semaforów - skrajnie trudne jest udowodnienie, że program taki jest prawidłowy.

Ci, którzy znają Javę zdają sobie sprawę, że mechanizmy operacji synchronizowanych wraz z metodami wait/notify są raczej niskiego poziomu oraz, że są podatne na błędy. Programista musi być świadomy szczegółów powiadamiania wątków, które w przypadku obiektów chronionych Ady są obsługiwane automatycznie.

10.3 Spotkania

Innym ważnym wymaganiem komunikacji między zadaniami jest przekazywanie informacji (danych) od jedno do drugiego zadania. W Adzie mechanizm ten jest znany pod nazwą spotkania. Dwa komunikujące się ze sobą zadania znajdują się w relacji klient-serwer. Klient żądający określonych usług musi znać zadanie serwera. Z kolei serwer udostępniający usługi będzie akceptował żądania od dowolnego klienta.

Ogólny wzorzec serwera to:

task Server is
   entry Some_Service (Formal: in out Data);
end;
 
task body Server is
begin
   ...
   accept Some_Service (Formal: in out Data) is
   ...                          -- instrukcje realizujące usługę
   end Some_Service;
   ...
end Server;

Specyfikacja serwera wskazuje, że ma on wejście Some_Service. Jest ono wywoływane przez zadanie klienta w ten sam sposób jak wejście obiektu chronionego. Różnica polega na tym, że kod do wykonania jest podany instrukcją accept, a jest wykonywany tylko jeśli zadanie serwera osiągnie tę instrukcję. Dopóki to nie nastąpi zadanie wywołujące jest wstrzymane. Gdy serwer osiągnie instrukcję accept, wykonuje ją korzystając z podanych przez klienta parametrów. Klient pozostaje wstrzymany do momentu zakończenia instrukcji accept, po czym następuje aktualizacja parametrów będących w trybach out lub in out.

Treść klienta mogłaby być taka:

task body Client is
   Actual: Data;
begin
   ...
   Server.Some_Service (Actual);
   ...
end Client;

Każde wejście posiada powiązaną ze sobą kolejkę. Jeśli zadanie wywołuje wejście serwera, a sam serwer nie czeka w instrukcji accept tego wejścia, to zadanie wywołujące zostaje umieszczone w kolejce. Z drugiej strony, jeśli serwer osiągnie instrukcję accept i brak jest zadań oczekujących w powiązanej z danym wejściem kolejce, to serwer zostaje wstrzymany. Instrukcja accept może pojawić się gdziekolwiek, na przykład wewnątrz gałęzi instrukcji warunkowej (if) lub wewnątrz pętli. Tak więc widzimy, że mechanizm jest bardzo elastyczny.

Spotkania stanowią mechanizm abstrakcji wysokiego poziomu (podobnie jak obiekty chronione) i jako takie są łatwe do wykorzystania w prawidłowy sposób. Odpowiadające mu mechanizmy kolejkowania programowane na niskim poziomie są trudne do realizacji.

Oto przykład w jaki sposób spotkania mogą być użyte do stworzenia usługi, na którą klient nie musi czekać. Pomysł polega na tym, że klient przekazuje serwerowi wejście do wywołania, gdy praca jest zakończona. Wpierw zadeklarujemy typ Mailbox:

task type Mailbox is
   entry Deposit(X: in Item);
   entry Collect(X: out Item);
end;
 
task body Mailbox is
   Local: Item;
begin
   accept Deposit(X: in Item) do
      Local := X;
   end;
 
   accept Collect(X: out Item) do
      X := Local;
   end;
end Mailbox;

Zadanie tego typu działa jak prosta skrzynka pocztowa. Element może być zdeponowany,a później odebrany. Klient przekazuje tożsamość skrzynki do serwera. Dzięki temu serwer może deponować elementy w skrzynce, zaś klient może je odbierać później. Potrzebny nam jest typ dostępowy:

type Mailbox_Ref is access Mailbox;

Zadania Server i Client przyjmą taką postać:

task Server is
   entry Request(Ref: Mailbox_Ref; X: Item);
end;
 
task body Server is
   Reply: Mailbox_Ref;
   Job: Item;
begin
   loop
      accept Request(Ref: Mailbox_Ref; X: Item) do
         Reply := Ref;
         Job := X;
      end;
 
      ...                               -- robota
 
      Reply.Deposit(Job);
   end loop;
end Server;
 
task Client;
 
task body Client is
   My_Box: Mailbox_Ref := new Mailbox;  -- utworzenie zadania skrzynki pocztowej
   My_Item: Item;
begin
   Server.Request(My_Box, My_Item);
   ...                                  -- coś można robić w czasie oczekiwania
   My_Box.Collect(My_Item);
end Client;

W praktyce klient może od czasu do czasu badać skrzynkę pocztową celem stwierdzenia, czy jakiś element jest już gotowy. Łatwo to można zrealizować korzystając z warunkowego wywołania wejścia:

select
   My_Box.Collect(My_Item);
   -- element odebrany z powodzeniem
else
   -- jeszcze nie gotowe
end select;

Ważne jest by uświadomić sobie, że zadanie agenta skrzynki pocztowej służy do wielu celów. Oddziela on operacje depozytu i odbioru, dzięki czemu serwer może zabrać się za następną czynność. Dodatkowo oznacza to, że nie musi nic wiedzieć o kliencie. Wywoływanie klienta bezpośrednio wymagałoby tego, by klient był typem zadaniowym szczególnego rodzaju, co byłoby wysoce niepraktyczne. Zadanie agenta skrzynki pozwala nam skupić się na własności, której wymaga klient, a konkretnie na istnieniu wejścia Deposit.

10.4 Ograniczenia

Pragma Restrictions,  która służy do wyłączania niektórych szczególnych mechanizmów języka była już wspomniana w rozdziałach: Bezpieczne programowanie obiektowe i Bezpieczne zarządzanie pamięcią.

W Adzie 2005 wiele ograniczeń odnosi się do mechanizmów wielozadaniowości. Mechanizmy te w Adzie są bardzo bogate, dostarczają cały szereg udogodnień niezbędnych do zaspokojenia potrzeb programowania różnego rodzaju aplikacji czasu rzeczywistego. Lecz pewne aplikacje są na tyle proste, że nie wymagają aż takiego wachlarza udogodnień. Oto próbka kilku ograniczeń, które mogą być zastosowane:

No_Task_Hierarchy
No_Task_Termination
Max_Entry_Queue_Length => n

Ograniczenie No_Task_Hierarchy zabrania deklarowania zadań wewnątrz innych zadań czy podprogramów. Tym samym wszystkie zadania znajdują się na w pakietach bibliotecznych. No_Task_Termination sygnalizuje, że wszystkie zadania działają bez końca: jest to normalne we wszelkich aplikacjach sterujących, gdzie każde zadanie ma nieskończoną pętlę, realizując pewne powtarzalne cele. Ostatnia z wymienionych pragm ustala limit na liczbę zadań, które mogą być umieszczane w jednym czasie w kolejce pojedynczego wejścia.

Zalety stosowania odpowiednich ograniczeń są dwojakie:

  • Ograniczenia mogą pozwolić na zastosowanie prostszego systemu czasu działania. Taki będzie mniejszy i szybszy, przez co będzie bardziej odpowiedni dla aplikacji krytycznych pod względem czasu i zajętości pamięci.
  • Ograniczenia mogą umożliwić udowodnienie poprawności programu (determinizm, brak zakleszczeń, zapewnianie nieprzekraczalnych terminów. To z kolei jest przydatne w aplikacjach krytycznych pod względem bezpieczeństwa.

Mamy do dyspozycji jeszcze wiele innych ograniczeń. Jednak większość dotyczy tych mechanizmów zadaniowości, których tutaj nie omawialiśmy.

10.5 Ravenscar

Szczególnie ważna grupa ograniczeń jest narzucona przez profil Ravenscar. W celu zapewnienia, że program będzie spełniał warunki profilu, piszemy:

pragma Profile(Ravenscar);

Próba wykorzystania wyłączonych cech (wymienionych poniżej) spowoduje wygenerowanie błędu w czasie kompilacji.

Kluczowym zastosowaniem profilu Ravenscar jest ograniczenie wykorzystania mechanizmów zadaniowych, uniemożliwiających stwierdzenie, że program jest przewidywalny. Profil ten został zdefiniowany w czasie warsztatów International Real-Time Ada Workshops, które odbyły się dwa razy w odległej wsi Ravenscar, na wybrzeżu Yorkshire w południowo-wschodniej Anglii.

Profil jest równoważny grupie pewnej liczby ograniczeń plus kilka innych pragm dotyczących np. szeregowania. Ta grupa ograniczeń zawiera kilka, już wcześniej wymienionych pragm, powodujących, że: brak jest hierarchii zadań, wszystkie zadania działają bez końca, a kolejki wejść mają limit ustalony na 1 (tzn. w danym momencie na danym wejściu może być zablokowane tylko jedno zadanie).

Efekt działania połączonych ograniczeń jest taki, że jest możliwe złożenie oświadczenia, iż program jest zdolny spełnić rygorystyczne wymagania procesu certyfikacji.

Żaden inny język programowania nie oferuje takiej niezawodności, która jest wymuszona uaktywnionym profilem Ravenscar w Adzie. Opis reguł i wykorzystania profilu w systemach dużej integralności znaleźć można w raporcie technicznym ISO/IEC [3].

10.6 Odmierzanie czasu i szeregowanie

Żadne omówienie zadaniowości Ady - nawet krótkie - nie może obejść się bez kilku słów na temat odmierzania czasu i szeregowania zadań.

Mamy do dyspozycji kilka instrukcji pozwalających na jego synchronizację z zegarem. Możemy wstrzymać działanie programu przez określony odcinek czasu (co nazywamy opóźnieniem względnym) lub możemy wstrzymać program do określonego czasu (opóźnienie bezwględne):

delay 2 * Minutes;
delay until Next_Time;

Zakładamy, że mamy już odpowiednie deklaracje Minutes i Next_Time. Małe opóźnienia względne mogą być użyteczne w sytuacjach, gdy wymagana jest interakcja, podczas gdy bezwzględne - do obsługi okresowych zdarzeń. Czas jako taki może być odmierzany albo zegarem czasu rzeczywistego (w którym to przypadku mamy zagwarantowaną pewną dokładność) lub też lokalny zegar ścienne, który być może będzie poddawany zmianom, na przykład ze względu na zmiany na czasu letni lub zimowy. W Adzie dodatkowo możemy uwzględniać strefy czasowe i sekundy przestępne.

Mamy również do dyspozycji kilka standardowych zegarów. Korzystając z nich, po upływie zadanego czasu można uaktywnić działania zdefiniowane przy pomocy procedur chronionych (programy obsługi). Są trzy rodzaje zegara. Jeden umożliwia monitorowanie czasu procesora wykorzystanego przez pojedyncze zadanie. Drugi dotyczy budżetu procesora grupy zadań. Trzeci, natomiast, skupia się na czasie odmierzanym zegarem czasu rzeczywistego. Program obsługi jest przypisywana do zdarzenia czasowego przy pomocy wywołania procedury takiej jak Set_Handler.

Zilustrujemy to przykładem programu do odmierzania czasu gotowania jajka. Deklarujemy obiekt chroniony Egg:

protected Egg is
   procedure Boil(For_Time: in Time_Span);
 
private
   procedure Is_Done(Event: in out Timing_Event);
   Egg_Done: Timing_Event;
end Egg;
 
protected body Egg is
   procedure Boil(For_Time: in Time_Span) is
   begin
      Put_Egg_In_Water;
      Set_Handler(Egg_Done, For_Time, Is_Done'Access);
   end Boil;
 
   procedure Is_Done(Event: in out Timing_Event) is
   begin
      Ring_The_Pinger;
   end Is_Done;
end Egg;

Konsument może teraz napisać:

Egg.Boil (Minutes (4));     -- podczas oczekiwania na jajko można poczytać gazetę

Gdy jajko będzie gotowe włączy się dzwonek.

W Adzie 2005 mamy do dyspozycji kilka strategii szeregowania zadań. Przy pomocy pragm możemy ustalić daną strategię dla wszystkich zadań w programie lub tylko tych, które wymagają szczególnego zakresu priorytetów.

  • FIFO_Within_Priorities
    Na danym poziomie priorytetu zadanie jest kolejkowane zgodnie z zasadą FIFO. Dodatkowo, zadanie to może zostać wywłaszczone przez inne, posiadające wyższy priorytet.
  • Non_Preemptive_FIFO_Within_Priorities
    Na danym poziomie priorytetu zadanie wykonuje się do końca, do momentu zablokowania (na zasobie) lub do wykonania instrukcji delay. Zadanie nie może zostać przełączone przez inne zadanie o wyższym priorytecie. Ten rodzaj strategii jest często wykorzystywany w aplikacjach o wysokiej integralności.
  • Round_Robin_Within_Priorities
    Na każdym poziomie priorytetu zadania wykonywane są w ustalonych odcinkach czasowych, a potem przełączane. Bardzo popularna strategia, wykorzystywana od zarania dziejów programów współbieżnych.
  • EDF_Across_Priorities
    EDF - strategia Earliest Deadline First. W pewnym zakresie priorytetów dla każdego zadania określany jest termin ostatecznego zakończenia obliczeń (deadline). Zadanie z najmniejszą wartością deadline jest wybierane jako pierwsze do wykonania. Stosunkowo "młoda" strategia, w przypadku której można matematycznie udowodnić korzyści dotyczące wykorzystania procesora.
Mamy w Adzie również bogate mechanizmy dotyczące ustalania i zmiany priorytetów zadań oraz tzw. pułapy priorytetów (ceiling priority) obiektów chronionych. Ostatni mechanizm skutecznie przeciwdziała problemom związanym z inwersją priorytetów (co obszernie opisano w [4]).
Rozdział 9: Bezpieczna komunikacja Rozdział 11: Spark - Uwierzytelnione bezpieczeństwo
Tekst oryginalny w języku angielskim - pdf2
Zmieniony: Czwartek, 25 Marzec 2010 11:08  

Dodaj swój komentarz

Imię:
Temat:
Komentarz: