Pobieranie prezentacji. Proszę czekać

Pobieranie prezentacji. Proszę czekać

Programowanie rozproszone ADA 95. Wstęp Ada 95 jest uniwersalnym językiem oprogramowania przeznaczonym do tworzenia oprogramowania dużej skali. Rozszerzenie.

Podobne prezentacje


Prezentacja na temat: "Programowanie rozproszone ADA 95. Wstęp Ada 95 jest uniwersalnym językiem oprogramowania przeznaczonym do tworzenia oprogramowania dużej skali. Rozszerzenie."— Zapis prezentacji:

1 Programowanie rozproszone ADA 95

2 Wstęp Ada 95 jest uniwersalnym językiem oprogramowania przeznaczonym do tworzenia oprogramowania dużej skali. Rozszerzenie wersji Ada 83. Tworzenie języka Ada 83 na zlecenie Departamentu Obrony USA. Uznana przez ANSI (American National Standards Institute). Schemat procesu wytwarzania oprogramowania Nieformalny opis problemu Analiza problemu Specyfikacja problemu Analiza wymagań na oprogramowanie Specyfikacja oprogramowania Projekt Implementacja Oprogramowanie Wdrażanie i eksploatacja

3 Ada jest związana z: - bezpośrednio z fazą implementacji, - pośrednio z fazą projektowania (w części język projektowania – obiektowość (języka), - fazą wdrażania i eksploatacji- łatwość modyfikacji oprogramowania w zależności od zmieniających się wymogów użytkowych. Własności języka ADA 95 - obiektowość – usprawnia fazę implementacji, możliwość implementacji fragmentów programu przez niezależne zespoły programistyczne - mechanizm rozłącznej kompilacji, - możliwość łączenia programów napisanych w innych językach, własność polimorfizmu statycznego i dynamicznego (Ada 95), - mechanizmy tworzenia programów równoległych i programów rozproszonych (Ada 95).

4 Zastosowania - asynchroniczne i synchroniczne systemy współbieżne rozproszone - tworzenie systemów czasu rzeczywistego, - specyficzną klasą zastosowań są systemy związane z bezpieczeństwem: - systemy nawigacyjne w lotnictwie, - systemy sygnalizacyjne w kolejnictwie - systemy medyczne. Struktura logiczna programu Podstawowe jednostki programowe: - podprogramy (procedury i funkcji) - Pakiety - jednostki rodzajowe (rodzajowe podprogramy i pakiety) - Zadania - obiekty chronione

5 Struktura fizyczna programu Strukturyzacja fizyczna programu wiąże się z potrzebami programowania w dużej skali: - mechanizmy rozdzielnej kompilacji - wielokrotne używanie komponentów oprogramowania poprzez tworzenie własnych bibliotek Jednostkami kompilacji mogą być dowolne jednostki programowe. Jednostkami bibliotecznymi mogą być podprogramy (procedury i funkcji), pakiety, jednostki rodzajowe (rodzajowe podprogramy i pakiety) Jednostki biblioteczne -> potomne jednostki biblioteczne Specyfikacja i treść jednostek bibliotecznych są oddzielnymi jednostkami kompilacyjnymi. Pomiędzy jednostkami kompilacyjnymi można określać hierarchiczną relacje, która określa wzajemną widzialność jednostek. Zestaw standartowych pakietów. Ada 95 w stosunku do wersji Ada 83 wprowadza jeszcze jedną formę strukturyzacji programu – rozproszenie programu. Podział programu na partycje, z których każda może być wykonywana na osobnym komputerze.

6 Przykład – pierwszy program with text_io; use text_io; procedure pierwszy is begin put_line(pierwszy program!); end pierwszy; -- specyfikacja kontekstu -- procedura główna programu -- standardowa procedura z -- pakietu Text_IO Oznaczenia w specyfikacji składni języka bold - słowo kluczowe { }- dowolny ciąg powtórzeń [ ] - element opcjonalny \ \- wybór jednego z elementów składowych |- alternatywa ::=- reguła produkcji...- składnik identyczny

7 Identyfikatory W identyfikatorze małe i duże litery są nierozróżnialne. Identyfikator := Litera {[ Podkreślnik ] litera | cyfra} Tablica a b1a_1lancuch_1 Liczby 987_657_333 równoważne _592 2E e Literały znakowe i napisowe A, F, 1, ada 95,lancuch

8 Typy Typ składa się z dwóch elementów: - skończonego zbioru wartości A 1,..., A n - zbioru funkcji f 1,..., f k Definicja typu type Identyfikator_typu is definicja typu type tydzien is (pn, wt, sr, cz, pt, so, ni) Zawężenie użytkowania typu type T1 is private type T1 is limited private -- ograniczenie widoku typu – użytkownik nie zna struktury typu Definicja podtypu subtype Identyfikator_podtypu is Identyfikator_typu range [zawężenie] subtype dni_robocze is tydzien range pn.. pt subtype Litera is Character range a..h subtype godz is integer range 0..24

9 Uwaga: definicja podtypu nie wprowadza nowego typu. Ma to znacznie dla pojęcia zgodności typów. Dla dowolnego typu lub podtypu są określone dwa atrybuty: TBase – dla danego podtypu T zwraca nazwę tego typu bazowego, dla typu T jego nazwę. TSize – określa najmniejszą liczbę bitów, wymaganą do przedstawienia wartości typu T. put( "Najmniejszy integer: " ); put( Integer'First ); new_line; put( "Najwiekszy integer: " ); put( Integer'Last ); new_line; put( "Integer (w bitach): " ); put(Integer'Size ); new_line;

10 Trzy sposoby definiowania typu: - zawężenie zbioru wartości zdefiniowanego typu - rozszerzenie zbioru wartości zdefiniowanego typu (tylko typy znakowe) - zdefiniowanie nowego typu jako złożenie wartości typów wcześniej zdefiniowanych. (typy tablicowy i typ rekordowy) Typ całkowity ze znakiem Schemat definicji typu jest następujący: range proste_wyrażenie_statyczne..proste_wyrażenie_statyczne proste wyrażenie statyczne – to wyrażenie, którego wartość może być obliczona w trakcie kompilacji. proste wyrażenie statyczne musi być wyrażeniem statycznym dowolnego typu całkowitego z zakresu System.Min_Int, System.Max_Int gdzie System.Min_Int, System.Max_Int są stałymi, których wartość zależą od implementacji. W Adzie istnieje predefiniowany podtyp całkowity ze znakiem - Integer Typ całkowity resztowy Typ całkowity resztowy jest typem całkowitym bez znaku, w którym jest stosowana arytmetyka resztowa.

11 Schemat definicji typu jest następujący: mod Wyrażenie_statyczne; Wartość elementu Wyrażenie_statyczne jest nazywana modułem i musi być statyczną dodatnią wartością dowolnego typu całkowitego, nie większą niż wartości określone w pakiecie System. Typ całkowity resztowy oznacza typ całkowity z zakresem bazowym od zera do wartości (moduł-1). Przykłady deklaracji typów całkowitych resztowych : type Bajt_bez_znaku is mod 8; -- zakresem są liczby 0..7 type Słowo is mod 16; -- zakresem są liczby O.. 15 Jeśli wartości typu resztowego są argumentami binarnego operatora logicznego, to operatory binarne stosowane do tych wartości traktują je jako ciągi bitów, tzn. wykonuje się operację kolejno na każdej parze bitów np. A: Bajt_bez_znaku := 6; -- A jest ciągiem 0110 B: Bajt_bez_znaku := 5; -- B jest ciągiem 0101 C, D: Bajt_bez_znaku; C := A or B; -- wartość C będzie równa 7 D : = A and B; -- wartość D będzie równa 4

12 Typy rzeczywiste Typy rzeczywiste są typami pochodnymi pierwotnego typu universal_real. W związku z niemożnością reprezentowania wszystkich wartości zakresu wskazanego w definicji typu rzeczywistego stosuje się zawężenie dokładności, które określa: - liczbę cyfr dziesiętnych wymaganą do zapisu wartości typu (dokładność względna), lub - parametr delta, który określa różnicę pomiędzy kolejnymi wartościami typu (dokładność bezwzględna). Typy zmiennopozycyjne Schemat definicji typu zmiennopozycyjnego: digits wyrażenie_statyczne [ range proste_wyrażenie_statyczne_1.. proste_wyrażenie_statyczne_2 ] gdzie: - wyrażenie_statyczne określa wymaganą dokładność dziesiętną (minimalna liczba znaczących cyfr dziesiętnych). Wartość tego wyrażenia musi być liczbą typu Integer. - proste_wyrażenie_statyczne_i (i=l,2) powinno przyjmować wartość rzeczywistą. - dla każdego podtypu zmiennopozycyjnego Z jest definiowany atrybut Z'Digits, podający wymaganą dokładność dziesiętną dla elementów tego typu. Wartość tego atrybutu jest typu universal_integer.

13 W Adzie istnieje predefiniowany podtyp zmiennopozycyjny ze znakiem, nazwany Float, deklarowany w pakiecie Standard. Przykłady deklaracji typów zmiennopozycyjnych: type Temperatura is digits 3 range ; -- wartości będą zawierały 3 cyfry --dziesiętnych z podanego zakresu type Real is digits 8; -- obliczenia dla wartości tego typu -- będą prowadzone z dokładnością -- do 8-miu cyfr dziesiętnych subtype Zakres is Real range ; -- wartości typu Zakres należą do -- przedziału [0.0,3.0] Typy stałopozycyjne typy stałopozycyjne: mają określoną stałą dokładność reprezentacji liczby. Jest to wartość absolutna, nazywana parametrem delta typu stałopozycyjnego. zbiór wartości typu stałopozycyjnego składa się z liczb będących całkowitą wielokrotnością liczby nazywanej ziarnem typu.

14 Przykłady deklaracji typów stałopozycyjnych: type Pomiar is delta range ; -- wartości typu Pomiar będą -- zapisywane co w podanym zakresie type Pieniądze is delta 0.01 digits 9; -- pozwala zapisać spotykane -- zlotowe kwoty pieniężne' -- z dogodnością do groszy

15 Typy tablicowe Składowe typu są jednoznacznie identyfikowane przez jeden lub więcej wartości indeksów należących do podanych typów dyskretnych. Typ tablicowy może mieć zawężenie indeksów, co oznacza, że górne i dolne granice każdego z indeksów są określone. Element takiego typu jest nazywany tablicą zawężoną, a schemat deklaracji takiego typu tablicowego przyjmuje postać: type Nazwa typu tablicowego is array (Wskazanie_podtypu _dyskretnego I Zakres {Wskazanie podtypu _dyskretnego l Zakres}) of [aliased] Wskazanie_podtypu Można definiować także typ tablicowy nie zawężony, w którym granice indeksów nie są określone. Granice te wprowadza się deklarując obiekt (tablicę) tego typu (podaje się zawężenie indeksu lub wartości początkowe składowych tablicy). Schemat definicji dla typu tablicowego nie zawężonego jest następujący: type Nazwa typu tablicowego is array (Oznaczenie Podtypu range <> {,Oznaczenie Podtypu range <>}) of [aliased] Wskazanie Podtypu przy czym Oznaczenie_podtypu powinno wskazywać typ dyskretny.

16 Typy predefiniowane - pakiet Standard type Boolean is (False, True) -- predefiniowane operacje =, /=,, =, and, or, xor, not type Integer is zdefiniowany przez implementacje subtype Natural is Integer range 0.. IntegerLast; subtype Positive is Integer range 1.. IntegerLast; -- predefiniowane operacje dwuargumentowe =, /=,, =, abs, +, -, *, /, ** -- predefiniowane operacje jednoargumentowe +, - Spotkania w Adzie Komunikacja w Adzie jest: SYNCHRONICZNA NIEBUFOROWANA DWUKIERUNKOWA

17 Zadanie wywołująceZadanie przyjmujące Zadanie wywołujące przyjmujące musi znać: - nazwę zadania przyjmującego - nazwę miejsca spotkania Zadanie przyjmujące nie zna nazwy zadania wywołującego. Wejście 1 Wejście 2 Wejście 3 Wejście 4 Zadanie 1 Zadanie 2

18 Zaleta modelu asymetrycznego – łatwość programowania serwerów i nie musi być zmieniana t reść zadania serwera podczas dodawania nowego zadania wywołującego. Zadanie składa się z: specyfikacji – zawierać może jedynie deklaracje wejść, każde zadanie musi posiadać specyfikacje nawet jeśli jest ona pustej treści. Przykład specyfikacji : task Bufor is entry Wstaw (I : in integer); entry Pobierz (I : in integer) end Bufor; BuforWstaw(I); task body Bufor is begin select accept Wstaw (I : in integer) do --instrukcje end Wstaw; accept Sygnalizuj; end Bufor; Wywołanie wejścia przez zadanie:

19 Semantyka spotkania jest następująca: Zadanie wywołujące przekazuje swoje parametry wejściowe (in) do zadania przyjmującego i zostaje zawieszone do momentu zakończenia spotkania. Zadanie przyjmujące wykonuje instrukcje z treści accept Parametry wyjściowe (out) są przekazywane do zadania wywołującego Spotkanie jest teraz zakończone i żaden z procesów nie jest już wstrzymywany. with Ada.Text_IO; use Ada.Text_IO; procedure pierwsza is czyja_kolej : Integer:=1; Task P1; task body P1 is begin loop Put_line("sekcja lokalna zadania 2"); loop exit when czyja_kolej =1; end loop; Put_line("sekcja krytyczna zadania 1"); czyja_kolej:=2; end loop; end P1;

20 Task P2; task body P2 is begin loop Put_line("sekcja lokalna zadania 2"); loop exit when czyja_kolej =2; end loop; Put_line("sekcja krytyczna zadania 2"); czyja_kolej:=1; end loop; end P2; begin null; end Pierwsza; package pakiet_z_semaforami is task type Semafor_binarny is entry Czekaj; entry Sygnalizuj; end Semafor_binarny; task type semafor is entry Inicjalizuj(N : in Integer); entry Czekaj; entry Sygnalizuj; end Semafor; end pakiet_z_semaforami; /////////////////////////////////////////////////////////////////////

21 package body pakiet_z_semaforami is task body Semafor_binarny is begin loop select accept Czekaj; accept Sygnalizuj; or terminate; end select; end loop; end Semafor_binarny; task body semafor is licznik : integer; begin accept Inicjalizuj(N : in Integer) do licznik:=N; end Inicjalizuj; loop select when Licznik >0 => accept Czekaj do licznik:=licznik-1; end Czekaj; or accept Sygnalizuj do licznik:=licznik+1; end Sygnalizuj; or terminate; end select; end loop; end Semafor; end pakiet_z_semaforami; ////////////////////////////////////////////////////////////

22 with Ada.Text_IO; use Ada.Text_IO; with pakiet_z_semaforami; use pakiet_z_semaforami; procedure wzajemne_wykluczanie is s: Semafor_binarny; task Z1; task body Z1 is begin loop Put_line("sekcja lokalna zadania 1"); S.Czekaj; Put_line("sekcja krytyczna zadania 1"); S.sygnalizuj; end loop; end z1; task z2; task body z2 is begin loop Put_line("sekcja lokalna zadania 2"); S.Czekaj; Put_line("sekcja krytyczna zadania 2"); S.sygnalizuj; end loop; end z2; begin null; end wzajemne_wykluczanie;

23 Synchronizacja trzech i więcej spotkań Instrukcja zagnieżdżona accept task body Z1 is begin accept Synch_2 do -- Z2 wywołuje to wejście accept Synch_3 -- Z3 wywołuje to wejście end Synch_2; end Z1; Select w zadaniu wywołującym Zadanie wywołujące może się zawiesić jedynie czekając na jedno wejście. task body Z is begin loop select procesX.wejscie(... ); or delay 1.0 ; sekunda ciąg instrukcji; end select; end loop; end Z;

24 Jeśli wywołanie nie będzie przyjęte w zadanym czasie, to próba spotkania będzie zaniechana i wykonają się instrukcje po delay. Ankietowanie jednego lub wielu serwerów : task body Z is begin loop select serwer_1.wej(...); else null; end select; select serwer_2.wej(...); else null; end select; end loop; end Z; Ankietowanie co najwyżej jednego serwera : task body Z is begin loop select serwer_1.wej(...); else select serwer_2.wej(...); else end select; end loop; end Z;

25 Dynamiczne tworzenie zadań deklaracja typu zadaniowego, tworzenie za pomocą typu struktury danych zawierającej zadania, task type typ_bufora is entry Wstaw (I : in integer); entry Pobierz (I : out integer) end typ_bufora; Bufory : array(1..10) of typ_bufora; procedure P (buf : in typ_bufora) is I : Integer; begin buf.Wstaw(I); -- wywołanie wejścia z parametrem end P; Bufory(I+1).Wstaw(E)-- bezpośrednie wywołanie wejścia P(Bufory(J)); -- parametr będący zadaniem Priorytety i rodziny wejść pragma priority(N) N – literał całkowity

26 Priorytety wpływają na szeregowanie zadań. Nie mają wpływu na wybór gałęzi w instrukcji select. Jeśli jest kilka zadań o tym samym priorytecie to wybór jest losowy. Implementacja musi używać zarządcy z wywłaszczeniem, – jeśli zakończy się okres zawieszenia zadania o wysokim priorytecie, to zarządca musi na jego korzyść przerwać zadanie o niższym priorytecie. Rodzina wejść: type Priorytety is (niski, średni, wysoki); task Serwer is entry Żądanie(Priorytety) (.....); end Serwer; task body Serwer is begin loop select accept Żądanie(wysoki) (...)....; or when Żądanie(wysoki) Count = 0 => --liczba zadań czekających w kolejce accept Żądanie(średni) (...)....; or when Żądanie(wysoki) Count = 0 => Żądanie(średni ) Count = 0 => accept Żądanie(niski) (...)....; end select; end loop; end Serwer;

27 Jeśli rodzina wejść jest duża. type Priorytety is range ; for I in Priorytety loop select accept Żądanie(I) (...)....; else null; end select; end loop;

28 Podprogramy Podprogramy są podstawowymi jednostkami programowymi Ady. W Adzie istnieją dwa rodzaje podprogramów: procedury i funkcje. Procedury określają działania, zmierzające do uzyskania żądanego efektu. Funkcje określają działania, zmierzające do obliczenia pewnej wartości. Podprogramy pojęcia podstawowe Definicja podprogramu może składać się z dwóch części: deklaracji podprogramu, opisującej jego interfejs oraz treści, opisującej działanie podprogramu. [specyfikacja _ podprogramu;] -- deklaracja podprogramu specyfikacja _podprogramu is -- treść podprogramu; [ część deklaracyjna] begin ciąg_ instrukcji end; Specyfikacja opisuje interfejs podprogramu, tj. określa rodzaj podprogramu(procedura, funkcja), jego nazwę oraz parametry podprogramu. Specyfikacja funkcji dodatkowa wskazuje nazwę typu wartości obliczanej przez funkcję. Deklaracja oraz treść podprogramu muszą wystąpić w tej samej części deklaracyjnej jednostki otaczającej dany podprogram (wyjątek stanowią podprogramy deklarowane w obrębie pakietów, zadań, obiektów chronionych).

29 Funkcje Treść funkcji ma postać: function Nazwa funkcji[(Parametry_formalne)] return Nazwa_podtypu is -- specyfikacja [Część_deklaracyjna] begin ciąg instrukcji return Wyrażenie; end |Nazwa funkcji]; Deklaracja parametrów funkcji ma postać: Dok_1 ; |Dok_2;... ; Dok_N Gdzie Dok_i wygląda następująco: Nazwa: (in) Typ Jeżeli parametry są tego samego typu, dopuszczalny jest zapis: Narwal, Nazwa_2,..., Nazwa_K: [in] Typ W treści funkcji może wystąpić wiele instrukcji return (np. wewnątrz instrukcji wyboru). Wykonanie dowolnej z nich kończy działanie funkcji.

30 Przykładem treści funkcji jest: function Rok przestępny(Rok: Positive) return Boolean is - Funkcja Rok przestępny ma jeden parametr formalny o nazwie Rok. - Funkcja zwraca wartość True, jeżeli rok jest rokiem przestępnym oraz - wartość False w przeciwnym przypadku begin if Rok mod 4 /= O then return False; elsif Rok mod 100 /= O then return True; elsif Rok mod 400 /= O then return False; else return True; end if; end Rok _ przestępny; Treść podprogramu (w tym również funkcji) może być poprzedzona deklaracją. Deklaracja funkcji ma postać: function Nazwa funkcji [(Parametry formalne)] return Nazwa podtypu;

31 Wywołanie funkcji Przykład 1 : Wynik: Boolean;... Wynik:= Rok__przestepny (1996) ; -- zmienna Wynik przyjmie wartość True Przykład 2 : function Silnia (N: Integer) return Integer is begin if N=0 then return l ; else return N * Silnia(N - l); end if; end Silnia;... S: Integer;... S := Silnia (5); -- wartość zmiennej S, po wykonaniu funkcji Silnia. -- będzie równa 120

32 Formalne parametry funkcji mogą być dowolnego,nazwanego typu, również nie zawężonego.Gdy typ parametru jest typem nie zawężonym, zakres tego parametru jest ustalony na podstawie zawężonej wartości parametru aktualnego, np. Type Wektor is array ( Integer range <>) of Integeer; -- nie zawężona tablica liczb... function Suma (W: Wektor) return Integer is S: intager : = 0; Begin for j in W'Rangę loop -- J przyjmuje kolejne wartości z zakresu S := S + W (J) ; -- indeksów tablicy W ( atrybut Rangę ) end loop; return S; end Suma;... W: Wektor (1..3) := (1,2,3) ; S: Positive;... S := suma(W) ; -- wartość S, po wykonaniu funkcji Suma, -- będzie równa 6

33 Dany podprogram może zostać przemianowany, tzn. można mu nadać dodatkowo nową nazwę, obowiązującą w danym kontekście. Nowa nazwa nie przesłania poprzedniej, pozwala jedynie łatwiej korzystać z podprogramu. Ogólny schemat przemianowania funkcji jest następujący: function Nowa_nazwa_funkcji[(Parametry_formalne)] return Nazwa podtypu renames Poprzednia nazwa funkcji; Poniżej umieszczono przykład definicji funkcji uzyskanej w wyniku przemianowania: function Nowa_suma(W : Wektor) return Integer renames Suma; Wywołanie funkcji Nowa_suma (W) obliczy taką samą wartość jak wywołanie funkcji Suma (W).

34 Operatory Operatory są jedno lub dwuargumentowymi wbudowanymi funkcjami języka.W odróżnieniu od innych Funkcji, operatory mogą być używane w notacji wrostkowej i przedrostkowej. Oznacza to, że zapis X+1 jest równoważny wywołaniu funkcji "+" (X, l). W notacji przedrostkowej symbol operatora musi być ujęty w cudzysłów. Programista może używać symboli operatorów do nazywania własnych funkcji. Poniżej przedstawiono przykład definicji operatora "+" dla dodawania wektorów : type Wektor is array (Integer rangę o) of Float; function "+" (L, R: Wektor) return Wektor is W: Wektor (L' Rangę) ; -- tablica o dynamicznie ustalanym rozmiarze! begin for I in L' Range loop W(I) :=L(I) +R(I); -- operator "+" oznacza dodawanie liczb -- typu Float end loop; return W; end " + " ; L Wektor(1..3) := (1.0, 2.0, 3.0); R Wektor(1..3) := (-1.0, -2.0, -3.0); W Wektor (l.. 3) ; -- operator "+" jest używany do dodawania wektorów L ;= L + R; -- po Wykonaniu instrukcji, L będzie wektorem zerowym W ;="+"( L, R); -- alternatywne użycie operatora

35 Deklaracja własnych operatorów jest jednym z przykładów przeciążania podprogramów. Operator "/=", który zwraca wartość Boolean, może być zdefiniowany jedynie przez definicję komplementarnego operatora "=", tzn. zdefiniowanie operatora "=" oznaczają również niejawne zdefiniowanie operatora "/=". Procedury Treść procedury tworzy się według schematu: procedure Nazwa_procedury[(Parametry_formalne)] -- specyfikacja procedury is [Część_deklaracyjna] begin... --ciąg instrukcji, opisujący działanie procedury [return;] end [Nazwa_procedury); Deklaracja parametrów ma postać: Dok_1; Dok_2;...; Dok_N; Gdzie Dok_1 wygląda następująco : Nazwa: (Tryb) Typ Jeżeli parametry są tego samego typu, to dopuszczalny jest zapis: Nazwa_1, Nazwa_2,..., Nazwa_K: (Tryb} Typ

36 Każdy parametr formalny może wystąpić w jednym z trzech tzw. trybów przekazywania parametrów in, in out, out. Tryb przekazywania parametru oznacza kierunek przekazania danych podczas wywołania podprogramu. W przypadku, gdy tryb przekazywania parametrów nie jest ustalony jawnie, tryb in przyjmuje się za domyślny. W treści procedury może znaleźć się bezparametrowa instrukcja return. Jej wykonanie kończy wykonanie procedury, po czym sterowanie jest przekazane do jednostki, która wywołała procedurę. Przykład: procedure Zamień (X, Y; in out Float) is Tmp: Float; begin Tmp := X; X := Y; Y := Tmp ; end Zamień ; Deklaracja procedury jest tworzona według schematu: procedure Nazwa_procedury [(Parametry_formalne)] ; Deklaracja procedury Zamień ma zatem postać: procedure Zamień(X, Y: in out Float); Wywołanie procedury jest instrukcją o postaci: Nazwa_procedury [(Parametry_aktualne)]; Poniżej pokazano przykład wywołania procedury Zamień: A: Float := 1.0; B: Float := -1.0;... Zamień (A, B) ; -- po wykonaniu procedury wartość zmiennej A= -1.0, B = 1. 0

37 Przekazywanie parametrów do podprogramów Parametry aktualne mogą być przekazywane do podprogramu przez położenie lub przez nazwę. Przekazywanie parametrów przez położenie polega na ustawieniu parametrów aktualnych w listą deklaracji parametrów formalnych, np. : Function Suma (A, B : Integer) return Integer;... W, S: Integer;... W : = Suma (5,6) ; -- parametr formalny A zostanie powiązany z wartością 5, -- a parametr B z wartością 6 S : = W ; W : = Suma (S,6); -- parametr A zostanie powiązany z wartością zmiennej S, -- a parametr B z wartością 2 Przekazywanie parametrów przez nazwę polega na tym, że parametr aktualny jest poprzedzony nazwą powiązanego z nim parametru formalnego oraz symbolem "=>", np.: S : = Suma(A=>5, B=>6) ; -- parametr A zostanie powiązany z wartością 5, -- a parametr B z wartością 6 S : = Suma(A=>W, B=>2) ; -- parametr A zostanie powiązany z wartością -- zmiennej W, a parametr B z wartością 2 S : = Suma(B=>6, A=>5) ; S : = Suma(B=>2, A=>W) ;

38 Przykład: procedure P(X: Integer; Y, Z: in out Float); A: Float := 2.0; B: Float := -3.5; P(3, Z=>A, Y=>B); Dla parametrów przekazywanych w trybie in można określić domyślne wartości początkowe (analogicznie do początkowych wartości deklarowanych zmiennych). Para­metry, dla których określono wartości domyślne, mogą zostać opuszczone w wywołaniu podprogramu. Można jednak w zwykły sposób zmienić ich wartości. W definicjach operatorów (np. "+") nie mogą wystąpić deklaracje parametrów domyślnych. Przykład: function Suma(A: Float := 3.14; B: Float := 0.0) return Float; W: Float;... W = Suma; -- parametr A przyjmie wartość 3.14, B wartość 0.0 W = Suma(2.0); -- parametr A przyjmie wartość 2.0, B wartość domyślną 0.0 W = Suma(B=>2.0); -- parametr A przyjmie wartość domyślną 3.14, -- B nową wartość 2.0

39 Przeciążanie podprogramów Użytkownik może zdefiniować wiele podprogramów o tej samej nazwie i różnym działaniu. Jeśli różne podprogramy mają takie same nazwy i różne parametry lub w przypadku funkcji różne typy wyników, mamy do czynienia z przeciążaniem podprogramów. nazywanym również statycznym polimorfizmem. Jeśli podprogramy mają identyczne nazwy, typy parametrów i wyniku (w przypadku funkcji) mówimy o przesłanianiu podprogramów. Wybór przeciążonego podprogramu jest dokonywany statycznie, na podstawie typów. parametrów aktualnych. W przypadku funkcji, których specyfikacje różnią się jedynie typem zwracanego wyniku, o wyborze podprogramu decyduje kontekst wywołania. Przykładem przeciążania podprogramów jest: type Dni_tygodnia is (pn, wt, sr, cz, pt, sb, nd) ; type Dni robocze is (pn, wt, sr, cz, pt); procedure Oblicz(D: Dni_tygodnia); -- 1) procedura Oblicz procedure Oblicz (D: Dni_robocze) ; -- 2) przeciążona procedura Oblicz Wywołania procedury Oblicz mogą być następujące: Oblicz(sb); -- wywołanie procedury 1) Oblicz (Dni robocze ' (wt) ); -- wywołanie procedury 2) Kwalifikacja typu parametru aktualnego w drugim wywołaniu procedury Oblicz jest niezbędna w celu ustalenia, którą z przeciążonych procedur wywołać (tu zostanie wywołana procedura oznaczona 2).

40 Ilustracją jest przykład przeciążenia operatora+ dla typów pochodnych; type T1 is new integer; type T2 new integer;... A1 T1; B1 T2;... A := A + 1; -- przeciążony + dla T1 B := B + 1; -- przeciążony + dla T2 A := B; -- niepoprawne przypisanie (T1 jest innym typem, niż T2) Podprogramy rodzajowe Każdy podprogram może mieć dwa rodzaje parametrów parametry zwykłe, takie jak w Pascalu, czy C, wykorzystywane w trakcie wykonania programu, deklarowane za nazwą podprogramu, oraz parametry rodzajowe, wykorzystywane na etapie kompilacji. Podprogramy z parametrami rodzajowymi nazywa się podprogramami rodzajowymi. Parametry rodzajowe zwiększają możliwość wielokrotnego użycia tego samego podprogramu w różnych kontekstach. Podprogram rodzajowy pełni rolę wzorca, na podstawie którego, w czasie kompilacji, są konkretyzowane różne, zależne od parametrów, deklaracje danego podprogramu. Definicja programu rodzajowego składa się z deklaracji oraz treści. Deklaracja podprogramu rodzajowego ma postać generic -- deklaracja podprogramu rodzajowego [Formalne_parametry_rodzajowe] Specyfikacja_podprogramu_rodzajowego;

41 Schemat składni, opisujący treść podprogramu rodzajowego jest identyczny jak dla zwykłego podprogramu. Formalnymi parametrami rodzajowymi podprogramu mogą być: obiekty, typy. podprogramy. Poniżej podano przykład definicji podprogramu rodzajowego uzyskanego przez dodanie parametru rodzajowego do procedury Zamień: generic type Typ rodź is private; -- deklaracja parametrów procedurę Zamień(X, Y: in out Typ_rodz); -- rodzajowych procedurę Zamień(X, Y: in out Typ_rodz) is -- specyfikacja Tmp: Typ__rodz ; -- podprogramu Begin -- rodzajowego Tmp := X; -- treść podprogramu X := Y; -- rodzajowego Y := Tmp; end Zamień;

42 Aby wykorzystać procedurę Zamień, należy ją ukonkretnić. Konkretyzacja podprogramu parametryzowanego obiektem i (lub) typem ma postać: procedurę Nazwa__podprogramu ukonkretnionego is new Nazwa_podprogramu_rodzajowego[(Parametry_aktualne)] ; Aktualne parametry rodzajowe mogą być kojarzone z parametrami formalnymi zarówno" przez położenie jak i przez nazwę. Przykładowe konkretyzacje procedury Zamień mogą być następujące: procedure Z1 is new Zamień(Integer); --konkretyzacja procedury typem Integer procedure Z2 is new Zamień(Float); -- konkretyzacja procedury typem Float procedure Z3 is new Zamień(Typ_rodz => Moj_typ) ; -- konkretyzacja procedury Zamień typem Mój typ Obiekty rodzajowe mogą być wykorzystywane do przekazania wartości, czy zmiennej do lub z podprogramu rodzajowego. Deklaracja obiektów, będących parametrami rodzajowymi podprogramu ma postać: Nazwa_l, Nazwa_2,..., Nazwa_N : [Tryb] Typ [:= Wartość_domyślna] ;

43 Wyjątki Zgłoszenie wyjątku powoduje zaniechanie (przerwanie) normalnego wykonania programu Programista może jednak zdefiniować pewne akcje (obsługą wyjątku), które mają być wykonane jako reakcja na wystąpienie danego wyjątku. Niektóre wyjątki są predefiniowane w języku, inne mogą być deklarowane przez programistę. Wyjątki predefiniowane zgłaszane są automatycznie, jeżeli nastąpi naruszenie zasad określonych przez semantykę języka, np. gdy zostanie przekroczony zakres wartości danego typu. Wyjątki zadeklarowane w programie zgłaszane są instrukcją raise w warunkach określonych przez programistę. Wyjątki predefiniowane W Adzie istnieją cztery predefiniowane wyjątki (zadeklarowane w pakiecie standard): Constraint Error, Program Error, Storage Error i Tasking_Error. Wyjątek Constraint_Error jest zgłaszany, m.in., gdy: nastąpi wywołanie podprogramu, w którym wartość aktualna parametru typu wskaźnikowego (przekazywanego w trybie in lub in out) jest równa null, drugi parametr operacji /, mod, rem jest równy zero, wartość indeksu tablicy przekracza zakres dopuszczalnych wartości, wartość skalarna przekroczy zakres bazowy swojego typu. Wyjątek Program_Error jest zgłaszany, m.in., gdy: w programie następuje odwołanie do niedostępnego bytu lub widoku, wywoływany jest podprogram lub wejście obiektu chronionego, którego deklaracja nie została jeszcze opracowana, wykonanie funkcji nie zakończyło się instrukcją return.

44 Wyjątek Storage_Error jest zgłaszany wtedy, gdy brakuje pamięci do opracowania deklaracji lub wykonania instrukcji języka (np. do obliczenia alokatora). Wyjątek Tasking_Error jest zgłaszany wtedy, gdy komunikacja z jakimś zadaniem jest niemożliwa (zadanie zostało już usunięte). Programista ma możliwość wyłączenia niektórych predefiniowanych sprawdzeń wykonywanych w czasie wykonania programu za pomocą pragmy Suppress Użycie pragmy Suppress poprawia efektywność wykonania programu (eliminuje dodatkowe czynności sprawdzające kod wynikowy jest krótszy), przerzuca jednak odpowiedzialność za poprawność programu na programistę. Deklarowanie i zgłaszanie wyjątków Deklaracja wyjątków ma postać: Nazwa _wyjątku l. Nazwa _wyjątku 2,..., Nazwa _wyjątku N.; Exception; Przykładami deklaracji wyjątków są: Blad: exception; Package Stos is Przepełnienie_bufora: exception; Begin... end Stos; Blad_odczytu, Blad_zapisu: exception;

45 Przemianowania wyjątku Przepełnienie _bufora zadeklarowanego pierwotnie w pakiecie o nazwie Stos można dokonać następująco: Declare use Stos Przepełnienie: exception renames Stos.Przepełnienie Bufora; Begin... end; Wyjątki zadeklarowane przez użytkownika są zgłaszane instrukcją raise, po której występuje nazwa wyjątku. Programista określa, jakie warunki muszą zostać spełnione, aby dany wyjątek został zgłoszony. Poniżej umieszczono przykład zgłoszenia wyjątku Blad przy próbie dzielenia przez zero: procedure Dzielenie(Dzielna, Dzielnik: Float; Wynik: out Float) is Bląd: exception; begin if Dzielnik =0.0 then raise Błąd; -- zgłoszenie wyjątku Błąd, gdy Dzielnik jest równy 0.0 else Wynik := Dzielna/Dzielnik; end if; end Dzielenie;

46 Obsługa wyjątków Wykonanie instrukcji lub opracowanie deklaracji może spowodować zgłoszenie wyjątku Po zgłoszeniu wyjątku normalne wykonywanie programu jest zaniechane, a sterowanie jest przekazywane do zdefiniowanej przez programistę strefy obsługi wyjątków. Strefa obsługi wyjątków może wystąpić w dowolnym bloku instrukcji, w zasięgu deklami |i danego wyjątku. Swoje strefy obsługi wyjątków mogą mieć: podprogramy, treści pakietów, instrukcje bloku, treści zadań, treści wejść w obiektach chronionych i instrukcje accept. Strefę obsługi umieszcza się zawsze na końcu bloku, po ostatniej normalnie wykonywanej instrukcji. Ilustruje to poniższy schemat: Instrukcja [exception -- strefa obsługi wyjątków Segment_obslugi_wyjątków l Segment_obsługi_wyjątków_N ] Każda strefa obsługi wyjątków musi zawierać co najmniej jeden segment obsługi wyjątków. Segment obsługi wyjątków ma postać: when Nazwa_wyjątku => Instrukcja (...} lub when Nazwa wyjątku l l Nazwa wyjątku 2 |... i Nazwa wyjątku N Instrukcja {...}

47 Nazwy wyjątków umieszczone w jednej strefie obsługi wyjątków muszą być unikatowe. Zbiory wyjątków należących do różnych segmentów muszą być rozłączne. Ostatni segment obsługi wyjątków może zawierać kluczowe słowo others, które oznaczą wszystkie wyjątki zgłoszone w danym bloku, lecz nie wymienione w poprzednich segmentach obsługi, włącznie z wyjątkami nie nazwanymi w bieżącym kontekście: when others => Instrukcja {...) W segmencie obsługi others obsłużone zostaną wyjątki: predefiniowane, nazwane w danym kontekście, lecz nie wymienione w innych segmentach obsługi wyjątków, wyjątki propagowane z jednostek dynamicznie zagnieżdżonych w bloku, w którym wystąpił segment obsługi others Obsługę wyjątku można wykorzystać do przywrócenia stanu systemu (o ile to możliwe) do stanu sprzed pojawienia się wyjątku lub do zapobiegania skutkom wyjątku. Na przykład obsługa wyjątku Blad dla nieco zmodyfikowanej procedury Dzielenie musiałaby być następująca:

48 procedure Dzielenie(Dzielna, Dzielnik: Float; Wynik: out float; OK: in out Boolean) is -- zakłada się. że parametrowi aktualnemu, podstawianemu za parametr formalny OK -- będzie przypisana wartość True przed wywołaniem procedury Dzielenie Błąd: exception; Begin... raise Błąd; exception when Błąd => OK := Faise; end Dzielenie; X, Y, W: Float; OK: Boolean := True;... Dzielenie(X, Y, W, OK) ; if OK then wynik W obliczony poprawnie Po wykonaniu segmentu obsługi wyjątku, sterowanie przekazywane jest na koniec bloku, który obsłużył dany wyjątek.

49 Przykład: procedurę Dzielenie(Dzielna, Dzielnik: Float; Wynik: out Float; OK: in out Boolean) is -- zakłada się, że parametrowi aktualnemu, podstawianemu za parametr formalny OK -- będzie przypisana wartość T'rue przed wywołaniem procedury Dzielenie begin Wynik ; " Dzielenia/dzielnik; exception when Constraint_Error => ok.:= false; end dzielenie; W przypadku, gdyby w procedurze Dzielenie nie wystąpiła strefa obsługi wyjątków, wyjątek Constraint_Error byłby propagowany do jednostki, w której ta procedura została wywołana.

50 Propagacja wyjątków W przypadku zgłoszenia wyjątku sterowanie jest przekazywane do strefy zawierającej obsługi wyjątku, zadeklarowanego w bloku, którego wykonanie spowodowało pojawienie się wyjątku. Jeżeli w danym bloku nie ma segmentu obsługi zgłoszonego wyjątku lub jeżeli w czasie wykonania segmentu obsługi zgłoszony zostanie inny wyjątek, to następuje automatyczne zgłoszenie wyjątku w bloku dynamicznie otaczającym blok zawierający wyjątek. Zgłoszenie danego wyjątku w nowym kontekście nazywa się propagacją wyjątku. Oznacza to np., że wyjątek nie obsłużony w danym programie, będzie propagowany do jednostki, w której ten podprogram był wywołany Propagacja wyjątku trwa tak długo, aż zostanie znaleziony odpowiedni segment obsługa wyjątku. Jeżeli poszukiwanie segmentu obsługi nie powiedzie się, wykonanie programu zostanie przerwane z komunikatem o błędzie (np. nie obsłużony wyjątek Program Error). Wyjątki zgłoszone i nie obsłużone w treści zadania nie podlegają propagacji. Zgłoszenie wyjątku może nastąpić nie tylko podczas wykonywania instrukcji, lecz także podczas opracowywania deklaracji. W tym przypadku przyjmuje się zasadę propagacji wyjątku do jednostki dynamicznie otaczającej deklarację, której opracowanie nie po wiodło się. Programista może, w ramach obsługi danego wyjątku, wymusić jego propagację. Do propagacji wyjątku służy bezparametrowa instrukcja raise. Instrukcja ta pozwala obsługiwać ten sam wyjątek na wielu poziomach, tzn. przez wiele dynamicznie zagnieżdżonych jednostek programowych.

51 Poniżej przedstawiono przykład wykorzystania instrukcji raise: procedure Główna is X, Y: exception; procedure P is begin... raise X;... raise Y; exception when X | Y => Pierwotna obsługa wyjątków X i Y przez procedure P raise; -- Wymuszenie propagacji wyjątków X, Y end P; procedurę Q is begin P; exception when X |Y ->... – Wtórna (dodatkowa)obsługa wyjątków X i Y przez Q end Q; begin Q; end Głowna;

52 Typy wskaźnikowe ograniczone Ograniczenie typu wskaźnikowego polega na jego powiązaniu z określonym (przez implementacje), dynamicznym obszarem pamięci (ang.storage pool), odpowiednikiem sterty w innych językach programowania. W obszarze tym są przechowywane, utworzone przez alokatory, obiekty danego typu wskaźnikowego. Wartości ograniczonego typu wskaźnikowego mogą wskazywać jedynie na elementy związanego z nim dynamicznego obszaru pamięci. Deklaracja ograniczonego typu wskaźnikowego wygląda następująco: type Nazwa_typu_wakaźnikoweqo is access Wskazanie_podtypu; Przykładem deklaracji ograniczonego typu wskaźnikowego jest: type Wsk_Int is access Integer; -- zmienne typu Wsk_Int wskazują na -- obiekty zawierające liczby całkowite Przy definiowaniu struktury danych złożonej z rekordów (np. listy), w której jeden z elementów rekordu będzie wskazywał na rekord tego samego typu, deklaracja typu wskazywanego jest dwuetapowa. Spowodowane jest to koniecznością zadeklarowania każdego identyfikatora przed jego użyciem. Pierwsza deklaracja (częściowa) stwierdza, że identyfikator Element_listy jest nazwą typu. Druga deklaracja określa wymaganą, pełną definicję typu: type Element listy; -- częściowa deklaracja typu Element listy type Wsk na element listy is access Element listy; -- deklaracja typu wskaźnikowego type Element listy is -- pełna deklaracja typu Element listy record Dana: Integer; Następnik: Wsk na element listy; end record;

53 Utworzenie obiektu wskazywanego przez zmienną wskaźnikową wymaga użycia alokatora, który zwraca adres tworzonego obiektu. Dla zadeklarowanej zmiennej: Zmienna wskaźnikowa: Nazwa typu wskaźnikowego; użycie alokatora może być następujące: Zmienna wskaźnikowa : = new Wskazanie_podtypu['(Początkowe_wartości_wskazywane) ] ; Zmienne typu wskaźnikowego mogą przyjąć wartość null oznaczającą, że zmienna nic wskazuje na żaden obiekt. Po inicjacji, zmienne wskaźnikowe przyjmują wartość null, o ile programista od razu nie określi wskazywanej wartości. Przykładami deklaracji zmiennych wskaźnikowych są: N: Wsk Int; -- zmienna N ma wartość null L: Wsk Int : = new Integer'(0); -- zmienna wskazuje na liczba 0 P: Wsk_Int : = new Integer; -- zmienna P wskazuje mi liczbę -- o nieokreślonej wartości Przykładem deklaracji listy jest: Eleml, Elem2: Element_listy; El, E2 : Wsk_na_element_listy; -- zmienne El, E2 mają wartość Null... E3: Wsk_na_element_listy := new element_listy(1, null); E4: Wsk_na_element_listy := new element_listy(Elem1);

54 Zmienna E3 wskazuje na record, rekord którego składowe są zainicjowane odpowiednio wartościami l i null, Zmienna E4 wskazuje na rekord o wartościach określonych przez Elem1. Dla typów prostych notacja: Nazwa zmiennej wskaźnikowej.all oznacza wskazywaną wartość, np. zapis N.all:= 2; oznacza, że wartość zmiennej wskaźnikowej N nie ulega zmianie, zmienia się jedynie wartość wskazywana przez N. Zachodzi istotna różnica między wartościami typu wskaźnikowego i wartościami przez nie wskazywanymi. Przypisanie: Elem2 := Eleml; spowoduje skopiowanie wartości składowych rekordu Eleml do składowych rekordu Elem2, natomiast przypisanie: El := E2; spowoduje, że zmienna El będzie wskazywać na ten sam rekord, co zmienna E2 (w pamięci jest tylko jeden rekord, na który wskazują obie zmienne). Instrukcja przypisania służy utworzeniu kopii wartości rekordu wskazywanego: El.all := E2.all;

55 Typy wskaźnikowe ogólne Typy wskaźnikowe, które mogą wskazywać na zadeklarowane obiekty oraz podprogramy, nazywane są ogólnymi typami wskaźnikowymi. W tym podrozdziale omawia się typy wskaźnikowe, wskazujące na stale i zmienne Ady. Deklaracja ogólnego typu wskaźnikowego ma postać: type Nazwa_ogólnego_ typu_wskaźnikowego is access constant Wskazanie_podtypu; lub type Nazwa_ogólnego_ typu_wskaźnikowego is access all Wskazanie_podtypu; W deklaracji ogólnego typu wskaźnikowego musi wystąpić specyfikacja modyfikatora dostępu słowo all lub constant. Użycie modyfikatora all oznacza, że wskazywany obiekt może być czytany i modyfikowany za pośrednictwem zmiennej wskaźnikowej. Użycie modyfikatora constant oznacza, że za pomocą zmiennej wskaźnikowej można jedynie odczytywać wskazywane obiekty. Przykładami deklaracji ogólnych typów wskaźnikowych są: type Wsk_na_stale_Integer is access constant Integer; -- Wartości typu Wsk na stale Integer wskazują na stale typu Integer type Wsk na_zmienne Integer is access all Integer; -- Wartości typu Wsk_na_zmienne_Integer wskazują na zmienne typu Integer type Wsk na op is access all Operacja_Binarna'Ciass; -- Wartości typu Wsk na op wskazują na zmienne należące do typu klasowego -- Operacja Binarna

56 Zmienne wskaźnikowe mogą wskazywać na zadeklarowane obiekty pod warunkiem, że obiekty te są deklarowane jako obiekty aliasowane. Wartości wskaźników ustala się za pomocą atrybutów Access lub Unchecked_Access. Na użycie atrybutu Access nałożone są pewne ograniczenia. Mianowicie, atrybutu Access nie można użyć do ustalania wartości zmiennej wskaźnikowej, której typ ma szerszy zasięg, niż zasięg wskazywanego obiektu. Użycie atrybutu Unchecked_Access nie podlega tym ograniczeniom programista odpowiada za jego poprawne użycie. Użycie atrybutów Access oraz Unchecked_Access może być następujące: type Wsk_na_stala_Integer is access constant Integer; CPI, CPC: Wsk_na_stala_Integer; I: aliased Integer; -- słowo kluczowe aliased C: aliased constant Integer := 1815;... CPI : = I Access -- odczyt wskaźnika na aliasowaną zmienną I CPC: = C Access -- odczyt wskaźnika na aliasowaną stałą C type Wsk_na_zmienna_integer is access all Integer; IP : Wsk_na_zmienna_integer; I : aliased integer;... IP := I'Unchecked_Access; -- odczyt wskaźnika na zmienną I I : =I+1; -- zmiana wartości zmiennej I

57 Do ustalenia wartości wskazywanych przez zmienne ogólnego typu wskaźnikowego można również używać alokatorów tak samo, jak dla typów wskaźnikowych ograniczonych. Ciekawym zastosowaniem ogólnego typu wskaźnikowego jest możliwość zaprogramowania tablicy o wierszach różnej długości: package Uslugi_komunikacyjne is type Typ kodu wiadomości is rangę ; subtype Wiadomość is String; function Daj_wiadomosc(Kod_wiadomosci: Typ_kodu_wiadomosci) return Wiadomość; pragma Inline (Daj wiadomość) ; -- treść funkcji Daj_wiadomosc zostanie wstawiona podczas kompilacji -- w miejsce jej wywołań end Usługi komunikacyjne; package body Uslugi_komunikacyjne is type Obsługa wiadomości is access constant Wiadomość; -- Typ wskaźnikowy, wskazujący na obiekty typu Wiadomość aliasowane stałe -- typu Wiadomość Wiadomosc_0: aliased constant Wiadomość := "OK"; Wiadomość _l: aliased constant Wiadomość := "Włącz"; Wiadomość _2: aliased constant Wiadomość := "Wyłącz";... Tablica_wiadomosci: array (Typ_kodu_wiadomosci) of Obsługa wiadomości : = --Ustalenie wartości elementów tablicy (wskaźników na kolejne Wiadomości) (O => Wiadomosc_0'Access, 1 => Wiadomość _l'Access, 2 => Wiadomosc_2'Access,... etc. ) ;

58 function Daj_wiadomosc(Kod_wiadomosci: Typ_kodu_wiadomosci) return Wiadomość is begin return Tablica wiadomości(Kod wiadomości).all; end Daj wiadomość; end Usługi komunikacyjne; Aby otrzymać wiadomość o określonym kodzie (O "OK", l "Włącz" itd.) wystarczy wywołać z odpowiednim parametrem (kodem) funkcję Da j _wiadomość pakietu Uslugi_komunikacyjne. Pakiety Pakiety są podstawową jednostką strukturalizacji programów w Adzie. Pakiet jako podstawowa jednostka projektowania programu oznacza, że jest on jednostką dekompozycji programu, która ma służyć do podziału funkcji całego programu na części składowe. Pakiet jest kolekcją pewnych bytów programowych, które są dostępne według ścisłe określonych zasad. Przez inne jednostki programowe pakiet jest postrzegany jako jednostka świadcząca im pewne usługi. Pakiet jest też jednostką, która może być oddzielnie kompilowana, uruchamiana i testowana. Gotowy pakiet może być jednostką składową nie tylko programu, dla którego go pierwotnie opracowano, ale także – jako jednostka biblioteczna może być używany wielokrotnie do budowy innych programów.

59 Struktura pakietu Definicja pakietu składa się z dwóch jednostek składniowych: specyfikacji pakietu oraz treści pakietu. Podział na dwie części wiąże się z postulatem separacji specyfikacji usług od ich implementacji. Składnia specyfikacji przedstawia się następująco: package Nazwa_pakietu is deklaracje publiczne [private... ] -- deklaracje prywatne end [[Nazwa_jednostki_macierzystej.]Nazwa_pakietu]; Specyfikacja pakietu określa usługi oferowane przez pakiet innym jednostkom programowym. W pierwszej części publicznej zestawione są deklaracje bytów (stałe, zmienne. typy, podprogramy, zadania, pakiety, wyjątki, przemianowania, klauzule reprezentacji) które mogą być używane przez inne jednostki programowe (użytkowników pakietu). W drugiej części prywatnej są zestawione deklaracje bytów, z których użytkownik nie może korzystać bezpośrednio. Informacja o tych bytach jest potrzebna podczas, kompilacji programu. Byty prywatne są używane wyłącznie w treści pakietu. Czesi prywatna jest opcjonalna.

60 Przykładem specyfikacji pakietu jest: package Liczby wymierne is type Wymierna is record Licznik: Integer; Mianownik: Positive; end record; function "/" (X, Y:Integer) return Wymierna; -- konstrukcja liczby wymiernej function "=" (X, Y: Wymierna) return Boolean; function "+"(X: Wymierna) return Wymierna; -- operacja jednoargumentowa, przeciążona function "-"(X: Wymierna) return Wymierna; -- operacja jednoargumentowa, przeciążona tunction "i"(X, Y; Wymierna) return Wymierna; -- operacja dwuargumentowa, przeciążona function = (X,Y: Wymierna) return Wymierna; -- operacja dwuargumentowa przeciążona function * (X,Y: Wymierna) return wymierna;; -- operacja dwuargumentowa przeciążona function / (X,Y: Wymierna) return wymierna;; -- operacja dwuargumentowa przeciążona przekroczenie_zakresu : exception; end liczby_wymierne;

61 Pakiet Liczby Wymierne oferuje deklarację typu o nazwie Wymierna, który jest reprezentacją liczb wymiernych, oraz zestaw podstawowych działań (operacji) na liczbach wymiernych, wraz z wyjątkiem sygnalizującym ewentualne przekroczenie zakresu wartości, Składnia części implementacyjnej, czyli treści pakietu ma postać: package body Nazwa_pakietu is deklaracje [begin... ] -- ciąg instrukcji end [[Nazwa_jednostki_macierzystej.]Nazwa_pakietu] ; Szkielet przykładowej implementacji pakietu Liczby_Wymierne jest następujący: package body Liczby_wymierne is procedure Wspolny_mianownik(X, Y; in out Wymierna) is Mianowniki Positive; function NWW(x, y: Positive) return Positive is -- NWW - najmniejsza wspólna wielokrotność, begin... end; begin Mianownik := NWW(X.Mianownik, Y.Mianownik); X := (X.Licznik * Integer(Mianownik)/Integer(X.Mianownik), Mianownik) ; -- konwersja typu Mianownik na typ Integer Y := (Y.Licznik * Integer(Mianownik)/Integer(Y.Mianownik), Mianownik); end Wspólny mianownik;

62 function "/"(X, Y: Integer) return Wymierna is begin if Y = O then raise Przekroczenie_zakresu; end if; return(X, Positive(Y)); end; function "="(X, Y: Wymierna) return Boolean is begin... end; function "+" (X: Wymierna) return Wymierna Is begin... end; function "-" (X: Wymierna) return Wymierna is begin... end; function "+"(X, Y: Wymierna) return Wymierna is begin... end; function "-" (X, Y: Wymierna) return Wymierna is begin... end; function " * "(X, Y: Wymierna) return Wymierna is begin... end; function "/"(X, Y: Wymierna) retum Wymierna is begin... end; end Liczby wymierne;

63 Przykładowy szkielet procedur: Przykład1 procedure Obliczanie_pierwiastkow_rownania(...) is package Liczby wymierne is -- bezpośrednia deklaracja pakietu... end Liczby wymierne; package body Liczby wymierne is... end Liczby_wymierne; begin... end Obliczanie_pierwiastków_równania; Przykład 2 with Liczby_wymierne -- specyfikacja kontekstu procedure Obliczenie_pierwiastków_równania (...) is... begin... end Obliczenie_pierwiastków_równania;

64 Typy prywatne Ważnym mechanizmem systematyzującym projektowanie oprogramowania jest mechanizm abstrakcyjnych typów danych. Abstrakcyjny typ danych jest tu rozumiany jako zbiór (lub zbiory) wartości oraz związany z nim zbiór operacji. Istotą tego powiązania jest to, ze na wartościach ustalonego zbioru (lub zbiorów) można wykonywać tylko ustalone operacje. Pakiety wraz z typami prywatnymi stanowią mechanizm pozwalający na definiowanie abstrakcyjnych typów danych. Możliwość tę rozpatrzmy na przykładzie pakietu definiującego stos, na którym gromadzi się liczby całkowite. package Stos_liczb_calkowitych is type Stos is private; -- niepełna definicja typu Stos procedure Dopisz(S: in out Stos; X: in Integer) ; procedure Zdejmij(S: in out Stos; X: out Integer); function "="(S, T: Stos) return Boolean; Pusty, Przepełniony : exception; private Max; constant Integer := 100; type Tablica is array Integer rangę <> of Integer; typa Stos is record -- pełna definicja typu Stos S: Tablica (l..Max) ; -- zaważenie tablicy Szczyt : Integer range O.. Max := 0; end record end Stos_liczb_całkowitych;

65 Treść przykładowego pakietu przedstawia się następująco: package body Stos_liczb_całkowitych is procedure Dopisz(S: in. out Stos; X: in Integer) is begin if S.Szczyt = Max then raise Przepełniony; end if ; S.Szczyt := S.Szczyt + l; S.S(S.Szczyt) := X; end Dopisz; procedure Zdejmij(S: in out Stos; X: out Integer) is begin if S.Szczyt = 0 then raise Pusty; end if ; X := S.S (S.Szczyt) ; S.Szczyt : = S.Szczyt - l; end Zdejmij; function = (S, T: Stos) return is begin if S.Szczyt /- T.Szczyt then return False; end if; for I in 1..S.Szczyt loop if S.S(I) /- T.S(I) then return False; end if; end loop; return True; end "-"; end Stos_liczb_całkowitych;

66 Przykład: declare use Stos_liczb_calkowitych; Sl, S2: Stos; -- zmienne reprezentujące wartości dwóch różnych stosów X : Integer; begin Dopisz(Sl, 10); -- operacja dopisania elementu na pierwszy stos Dopisz (Sl, 20); -- operacja dopisania elementu na pierwszy stos... Zdejmij (S1, X); -- operacja zdjęcia elementu z pierwszego stosu... Dopisz (S2, 100); -- operacja dopisania elementu na drugi stos... if S1 = S2; -- operacja porównania stosów then... else S1 : = S2; -- operacja przypisania stosów... end if; end;

67 Przykład: package Modyfikacja_stosu is type Zmod_stos is private; -- niepełna definicja typu Pusty: constant Zmod_stos; -- tu nie ma możliwości przypisania -- wartości stałej Pusty private Max: constant Integer : = 100; type Tablica is array (Integer range <>) of Integer; type Zmod_stos is record -- pełna definicja typu S: Tabllica (1..Max); Szczyt : Integer range 0.. Max ; end record; Pusty: constant Zmod_stos : = (S => (others -> 0), Szczyt => 0); -- nadanie wartości stałej odroczonej Pusty end Modyfikacja stosu; Ograniczone typy prywatne Ograniczenie sposobu użytkowania bytów publicznych pakietu można jeszcze wzmocnić przez wykorzystanie konstrukcji ograniczonego typu prywatnego. Deklaracja ograniczonego typu prywatnego T ma postać: type T is limited private; Istota ograniczenia związanego z typem T polega na tym, że użytkownik pakietu nie może na zmiennych typu T wykonywać przypisywania wartości, czyli instrukcji przypisania, ani też porównywania ich wartości, czyli wykonywania operacji = oraz /=. type Stos is limited private;

68 Struktura programów Przykładem zawierania jednostek programowych (pakietów) jest: package Zewnetrzny is... package Wewnetrzny is -- jednostka programowa zagnieżdżona w jednostce... – Zewnetrzny procedure Srodek ; -- jednostka programowa zagnieżdżona w jednostce... – Wewnętrzny i jednostce Zewnetrzny end Wewnetrzny;... procedure Wypisz; -- jednostka programowa zagnieżdżona w jednostce... – Zewnetrzny end Zewnetrzny; Pakiet Zewnetrzny zawiera, zagnieżdżone w nim trzy jednostki programowe: pakiet Wewnetrzny, procedurę Środek (zawartą w pakiecie Wewnętrzny) oraz procedurę Wypisz.

69 Jednostki kompilacji i jednostki biblioteczne Jednostką kompilacji jest jednostka programowa oddzielnie przedstawiona do kompilacji. Jednostką kompilacji może być: pakiet, podprogram, obiekt chroniony lub jednostka rodzajowa, ale nie może być zadanie. Oddzielnie mogą być kompilowane specyfikacja i treść pakietu oraz obiektu chronionego. Przemianowania Rozbudowa programu, korzystanie z nazw bytów pochodzących z innych jednostek powoduje, że stosowane nazwy bytów mogą siać się nieczytelne. Ada pozwala nadać bytowi nowi| nazwie, przy zachowaniu wszystkich jego własności. Operację nadania nowej na/wy bytowi określa sil; przemianowaniem. Przykładem użycia przemianowanych bytów jest następująca procedura Główna: package body Stos is Max : constant : = 30; P: array(l..Max) of Integer; Top : Integer range 0..Max; procedure Odłóż(X: Integer) is begin... end Odłóż; function Pobierz return Integer is begin... end Pobierz;

70 begin Góra := 0; end Stos; with Stos; procedure Główna is K, L: Integer; procedure Połóż (X: Integer) renames Stos.Odłóż; function Weź return Integer renames Stos.Pobierz; begin... Połóż (K) ;... L : =Wez; end Główna; Przemianowanie pozwala uniknąć konfliktu nazw bytów pochodzących z różnych jednostek, a wynikających z zastosowania klauzuli use. Przemianowanie może również zastąpić stosowanie notacji kropkowej. Przemianowanie może być stosowane w przypadku operatorów, np.; function Dodaj (X, Y: Integer) return integer renames "+"; lub function "+" (X, Y: Integer) return Integer renames Dodaj;

71 Przemianowanie podprogramów musi być zgodne co do liczby, typów i trybów parametrów oraz typu wyniku w przypadku funkcji. Zgodność nie dotyczy nazw parametrów. Dzięki przemianowaniu można wprowadzać, zmieniać lub usuwać parametry i wyrażenia domyślne. Można przemianować obiekt typu złożonego, przy powtarzających się wyliczeniach wartości tego obiektu, np. niech będą dane deklaracje: type Data urodzenia is record Dzień: Integer range l..31; Miesiąc: Integer range l..12; Rok: Integer ; end record; type Osoba is record Imię: String; Nazwisko: String; Urodzony: Data urodzenia; end record; type Baza osób is array(1..20) of Osoba; Dane: Baza osób;

72 Rozpatrzmy fragment programu: for I in Dane'Range loop Put (Dane (I).Urodzony.Dzień); Put ("-"); Put (Dane(I).Urodzony.Miesiąc) ; Put ("-"); Put (Dane (I).Urodzony.Rok); Put (" r."); end loop; i ten sam fragment programu z wykorzystaniem przemianowania: for I in Dane'Range loop declare Data: Data urodzenia renames Dane(I).Urodzony; begin Put (Data.Dzień) ; Put ("-"); Put (Data,Miesiąc.) ; Put ("-"); Put (Data.Rok); Put (" r."), end; end loop;

73 Mechanizmy programowania obiektowego Przyjmuje się, że język programowania jest językiem obiektowym jeżeli: umożliwia deklarowanie typów obiektów, umożliwia deklarowanie operacji na obiektach, umożliwia enkapsulację (hermetyzację) obiektu i jego operacji, udostępnia mechanizm dziedziczenia, udostępnia mechanizm polimorfizmu statycznego i dynamicznego. W języku Ada obiektem jest stała lub zmienna. Obiekt przyjmuje wartości ustalonego typu i powstaje przez deklaracje lub przez użycie alokatora. Dla każdego typu mogą być definiowane elementarne operacje. Zazwyczaj, inne języki programowania obiektowego w definicji typu (klasy) zawierają tylko jego (jej) atrybuty oraz operacje na atrybutach. Ada nie wymusza tak ścisłej hermetyzacji (enkapsulacji) Enkapsulacja jest możliwa dzięki pakietom. Wykorzystując strukturę pakietu można zdefiniować w nim typ rekordowy określając jego atrybuty oraz elementarne operacje tego typu. Ponieważ, w ramach tego samego pakietu można zdefiniować również inne operacje,.nie związane z danym typem, stąd brak w Adzie ścisłej enkapsulacji.

74 Przykładem deklaracji typu w Adzie jest: package Baza is type Osoba is tagged record Nazwisko: String (l..30); Imię: String (l..30) ; end record; function Zmień_nazwisko (dane: in Osoba) return Osoba; -- operacja elementarna typu Osoba function Podaj_nazwisko (dane: in Osoba) return String (l..30); -- operacja elementarna typu Osoba procedure Komunikat; -- operacja me związana z typem Osoba end Baza; W przykładzie została zdefiniowana specyfikacja pakietu Baza zawierająca deklarację znakowanego typu rekordowego Osoba zawierająca dwa atrybuty Nazwisko i Imię. W specyfikacji pakietu Baza zostały zadeklarowane operacje elementarne typu Osoba, w postaci funkcji Zmien_nazwisko i Podaj_nazwisko oraz procedura Komunikat, która nie jest elementarną operacją typu Osoba. Możliwość zadeklarowania procedury Komunikat oraz innych procedur i funkcji, które nie są operacjami elementarnymi typu Osoba oznacza, że specyfikacja pakietu nie zapewnia ścisłej hermetyzacji. Obiektowe mechanizmy Ady są różne od podobnych mechanizmów występujących w innych językach obiektowych, jak np. C++ czy Smalltalk.

75 Dziedziczenie typów Obiektowe języki programowania zawierają mechanizm definiowania nowego typu na bazie istniejącego. Nowy typ nazywany jest typem pochodnym lub potomnym, natomiast typ, z którego powstał typ pochodny nazywany jest typem bazowym. Pojęcie pochodności typu należy odróżnić od pojęcia pochodności jednostki kompilacji opisanej poprzednim rozdziale. Pochodność typu jest związana z opisem poniżej dziedziczeniem typów. Typ pochodny może być typem bazowym dla kolejnego typu pochodnego. Jeśli typ T2 jest typem pochodnym typu T1 a typ T3 jest typem pochodnym typu T2, to typ 1 jest typem bazowym dla typu T2. a dla typu T3 typ T1 jest jego pośrednim poprzednikiem. Jeśli typ T4 jest typem pochodnym typu T2, to T2 jest typem bazowym typu T4, a typ t l jest jego pośrednim poprzednikiem. Typy T1, T2, T3 i T4 tworzą hierarchię, którą można przedstawić w postaci drzewa. Typ T1 jest korzeniem drzewa, którego gałęziami są typy T2, T3 i T4. Typ T2 jest korzeniem poddrzewa, które zawiera dwie gałęzie typy T3 i T4. T2 T1 T3T4

76 Typ pochodny dziedziczy strukturę i operacje elementarne typu bazowego. Mechanizm dziedziczenia uważa się za pełny, jeśli umożliwia: tworzenie typu pochodnego w oparciu o typ bazowy, przeciążanie operacji typu bazowego, rozszerzanie typu bazowego. Przy braku którejś z dwóch ostatnich możliwości mówi się o dziedziczeniu ograniczonym. Język Ada zapewnia dwa pierwsze mechanizmy dla dowolnego typu. Trzeci mechanizm jest możliwy tylko dla typu znakowanego. Typem znakowanym może być tylko typ rekordowy. Każdy typ pochodny od typu znakowanego jest z definicji typem znakowanym (nawet bez wyróżnienia jego definicji słowem kluczowym tagged), np.: type Punkt is tagged -- bazowy typ znakowy record Wspolrzedna_X: Float; Współrzędna Y: Float; end record; type Okrąg is new Punkt with -- typ pochodny record -- (również znakowy) promień: Float; -- dodana składowa end record; type Punkt_ Plaszczyzny is new Punkt -- typ pochodny (znakowany) with null record; -- bez nowych składowych

77 Zmiana struktury typu pochodnego może spowodować zmianę implementacji operacji odziedziczonych przez ten typ. Jeśli przyjmiemy, że dla typu Punkt została zdefiniowana operacja Pole: function Pole (Ob: in Punkt) return. Float is begin return 0.0; end Pole; to zostanie ona odziedziczona przez typy: Okrąg i Punkt_Plaszczyzny. Jednak, dla typu Okrąg implementacja operacji Pole może mieć postać: function Poie(Ko: in Okrąg) return Float is begin return Pi*Ko.Promień**2; end Pole; Funkcja Pole została przeciążona, tzn. w zależności od typu parametru (Punkt lub Okrąg) zostanie wyliczona odpowiednia wartość. Niech typ T1 będzie poprzednikiem typu T2. Istnieje możliwość przypisywania wartości obiektów typu T2 do obiektów typu T1 i odwrotnie. W tym celu należy dokonać odpowiedniej (zawężającej lub rozszerzającej) konwersji typów obiektów. Przy konwersji zawężającej następuje odrzucenie wartości składowych, które nie występują w typie poprzednika. Natomiast przy konwersji rozszerzającej należy podać wartości nowych składowych w kolejności ich deklarowania lub zastosować agregat.

78 Przykładowo, mając zadeklarowane zmienne: X: Punkt := (1.5, 0.7); K: Okrąg := (0.0, 0.0, 50.0); P: Punkt_plaszczyzny; możemy dokonywać konwersji: X : = Punkt (K) ; -- wartość składowej K. Promień będzie zignorowana K : = (X with 100.5) -- określenie wartości brakującej składowej w zmiennej X P : = (X with null record) ; -- przekształcenie do rekordu bez dodatkowych -- składowych Do konwersji typów można stosować agregat, np.: K := (X with Promień => 100.5); Ogólnie, przed słowem kluczowym with może wystąpić dowolne wyrażenie, wskazujące na obiekt typu poprzednika.

79 Klasy i polimorfizm Polimorfizm dynamiczny zapewniają tylko typy znakowane. Z typem znakowanym są związane dwa pojęcia: klasy typów i typu klasowego. Deklaracja dowolnego typu znakowego powoduje automatyczne, niejawne utworzenie klasy oraz deklaracje typu klasowego. Nazwa typu klasowego jest określana niejawnie przez atrybut class. Przykładem użycia typu klasowego jest: type Opis i s tagged -- typ znakowany record... end record; Obiekt_Klasowy : Opis'Class; -- Opis ' Class jest nazwą typu klasowego -- generowanego przez typ Opis Zmienna obiekt_Klasowy jest typu klasowego o nazwie Opis'class,jej wartościami są wartości typu Opis oraz wartości wszystkich typów pochodnych od typu Opis. Każda wartość typu klasowego ma ukryty znacznik, określający do którego z typów, w danej klasie typów znakowanych, należy ta wartość.Przy deklaracji obiektu typu klasowego jest konieczne jego zainicjowanie. Inicjacja konkretyzuje jeden z typów należący do odpowiedniej klasy. Informacja o dokonanej konkretyzacji (o wybranym typie) jest przechowywana w ukrytym znaczniku obiektu. Konieczność inicjowania wynika z potrzeb implementacji musi być określony obszar pamięci przydzielany obiektowi typu klasowego.

80 Typ klasowy może być typem parametrów formalnych podprogramów. Dzięki temu podprogram może być wywołany dla dowolnych obiektów typu klasowego, czyli parametr aktualny może być dowolnego typu z danej klasy typów, np.: with Nowy System Sygnałów; procedure Obsłuż sygnały(S: in out Sygnał"Cłass) is... begin... Zarejestruj (S) ; -- powiązanie zgodne z typem wskazanym w znaczniku... end Obsłuż sygnały; W procedurze Obsluz_sygnaly parametrem aktualnym może być dowolny obiekt należący do klasy typu Sygnał. Natomiast dla procedury Zarejestruj musi być wybrana konkretna jej implementacja zależna od typu parametru S. Wybór tej implementacji nastąpi w trakcie wykonywania programu, a dokładniej, wykonania procedury Obsłuż sygnały, i będzie zależał od typu aktualnego parametru S, przekazanego w ukrytym znaczniku, np.:

81 with Nowy_System_Sygnałów; procedure Główna is S1 : Sygnał_l ; S2 : Sygnal_2; S3 : Sygnal_3; SSpec; Sygnał Zagrożenia; Begin... Obsluz_sygnaly(Sl);... Obsluz_sygnaly(S2) ;... Obsłuż sygnaly(S3);... Obsluz_sygnaly(SSpec) ; end Główna; Każde z wywołań procedury Obsluz_sygnaly jest poprawne. W zależności od typu parametru aktualnego, przy wykonywaniu procedury Obsluz_sygnaly zostanie wykorzystana odpowiednia implementacja procedury Zarejestruj. Będą to kolejno implementacje dla typów: Sygnal_l, Sygnal_2, Sygnal_3 i Sygnal_Zagrozenia. Przykład jest ilustracją mechanizmu polimorfizmu dynamicznego.

82 Istnieje również możliwość zdefiniowania typu wskaźnikowego, wskazującego na obiekty typu klasowego. Rozpatrzmy przykład oparty, na wcześniej rozpatrywanym, systemie sygnałów. Niech każdy nie obsłużony sygnał trafia do jedynej w systemie kolejki sygnałów. Kolejka ta jest heterogeniczna, tzn. mogą do niej trafiać sygnały różnych typów. Aby obsłużyć tę kolejkę i sygnały w niej zawarte możemy zastosować następujące rozwiązanie; type Wskaźnik sygnału is access Sygnał'Class; procedure Obsluz_sygnaly is Następny sygnał: Wskaźnik sygnału; Begin... Następny sygnał :=... ; -- pobierz kolejny element z kolejkii Zarejestruj (Nastepny_sygnał.all) ; -- powiązanie zgodne i typem wskazanym w znaczni ku end Obsluz_sygnaly; Nie obsłużone sygnały zostały zapamiętane w kolejce przechowującej różne typy sygnałów. Do zarejestrowania każdego sygnału z tej kolejki służy procedura obsłuż_sygnały. Zmienna Następny_sygnał tej procedury wskazuje kolejno na sygnały, które mają zostać zarejestrowane. Wywołanie procedury Zarejetruj ze zmienną Następny_sygnał jako aktualnym parametrem wywołania spowoduje, że dla każdego rodzaju sygnału zostanie wywołana właściwa implementacja procedury Zarejestruj (zgodna ze znacznikiem).

83 Asynchroniczna zmiana wątku sterowania Czwarta forma instrukcji select, w skrócie ATC (ang. Asynchronoits Transfer of Control), powoduje asynchroniczną zmianę wątku sterowania. Jej składnia jest opisana następującym schematem: select \wywołanie wejścia l instrukeja_delay\ [instrukcja (...(] then abort instrukcja (...) end select; -- część przerywana Instrukcja składa się z dwóch części: przerywanej i, opcjonalnej, przerywającej Instrukcje części przerywającej są poprzedzone instrukcją wyzwalającą. Działanie instrukcji select polega na warunkowym wykonywaniu części przerywanej do momentu, gdy nastąpi jej przerwanie na skutek akcji wyznaczonej przez instrukcję wyzwalającą. Wtedy wykonuje się część przerywająca i kończy się działanie całej instrukcji. Jeżeli część przerywana zakończy się przed wystąpieniem akcji podanej w instrukcji wyzwalającej, wówczas oznacza to zakończenie całej instrukcji select (nie jest wykonywana cześć przerywająca"). Instrukcją wywołującą może być instrukcja delay [until], wywołanie wejścia zadania lub wywołanie wejścia obiektu chronionego. Akcją, powodującą przerwanie wykonywania części przerywanej jest odpowiednio: upływ czasu, podany jako parametr instrukcji delay [until] lub zakończenie instrukcji wywołania wejścia. Przykład1: select delay 1. b ; -- instrukcja wyzwalająca Put_Line ("Obliczenia nie zakończyły się w wyznaczonym czasie"); then abort -- część przerywana funkcja_ rekursywna (X, Y) ; end select;

84 Przykład2: with Sygnały; use Sygnały; with Ada.Real_Time; use Ada.Real_Time; Nadzorca: Sygnał; protected Dane_dzielone is procedure Pisz(D: in Dane); entry Czytaj (D: out Dane); private Wynik_obliczen: Dane; Wynik_dostepny: Boolean := False; end Dane_dzielone; task Konsument; task Iterator; protected body Dane_dzielone is procedure Pisz(D: in Dane) is begin Wynik_obliczen := D; Wynik_dostepny := True; end Pisz; entry Czytaj(D: out Dane) when Wynik_dostepny is begin D := Wynik_obliczen; Wynik_obliczen := False; end Czytaj; end Dane_dzielone;

85 task body Konsument is begin loop Dane_dzielone.Czytaj(Wynik) exit when Czas > Czekam; -- pobranie przybliżonych danych end loop; -- minął czas obliczeń Nadzorca. Nadaj ; -- żądanie przerwania obliczeń w Iteratorze Dane_dzielone. Czytaj (Wynik);-- pobranie ostatniego przybliżenia end Konsument; task body Iterator is begin Dane_dzielone.Pisz(Wynik); -- uzyskanie wyniku z minimalną -- żądaną dokładnością -- i zapisanie w obiekcie chronionym select Nadzorca.Czekam; then abort -- obliczenie kolejnej iteracji przybliżonego wyniku loop obliczenie Dane_dzielone. Pisz (Wynik) ; -- i zapisanie wyniku exit when Uzyskano_najlepszy_wynik; end loop; end select; end Iterator;

86 Awaryjne kończenie zadań Przeciwieństwem normalnego zakończenia zadania jest zakończenie awaryjne, wymuszone instrukcją abort. Instrukcja ma następującą składnię: abort nazwa_zadania {, nazwa_zadania}; Dowolne zadanie może awaryjnie zakończyć inne zadanie wykonując podaną instrukcję. Przykładem jest instrukcja abort: abort Zad_l, Moja_Tab(5), R2.Zadanie, Wsk_Zadl.all; Jeżeli zadanie jest kończone awaryjnie, to również kończone awaryjnie są wszystkie jego zadania potomne. Realizacja instrukcji abort zależy od stanu zadania, które ma być awaryjnie zakończone. Jeżeli zadanie jest zawieszone, np. realizowana jest instrukcja delay, zadanie czeka na aktywację lub na spotkanie, itp., to zostaje natychmiast zakończone. Jeżeli w momencie awaryjnego kończenia zadania nie jest ono zawieszone, to instrukcja abort powoduje, że ulegnie ono zakończeniu w najbliższym dogodnym momencie, zależnym od implementacji języka. Z takim zadaniem nie można się już więcej komunikować.

87 Przykłady programów w języku Ada 95 Przykład 1: Przykład ilustruje zastosowanie typu zadaniowego z wyróżnikiem i typu wskaźnikowe­go. Pojedyncze zadanie oblicza S!. Deklaracje typu zadaniowego Silnia i typu wskaźnikowego Wsk_silnia są zawarte w pakiecie Pakiet_silnia. Ma on następującą specyfikację: package Pakiet_silnia is task type Silnia (X: in Positive) is -- typ zadaniowy -- z wyróżnikiem entry Wynik (Wy: out Long_Integer) ; -- wejście do pobierania -- wyniku end Silnia; type Wsk_silnia is access all Silnia; -- typ wskaźnikowy end Pakiet silnia; Typ zadaniowy Silnia ma pojedyncze wejście Wynik (...) oraz wyróżnik x, typu Positive. Wejście Wynik (...) służy do pobierania wyniku obliczeń z zadania silnia. Przekazywanie argumentu do obliczeń jest realizowane w momencie kreacji i aktywacji zadania na podstawie typu wskaźnikowego Wsk_silnia i następuje przez podanie wartości wyróżnika z jakim zadanie ma być utworzone. Wynik obliczeń jest typu Long_integer, z zakresu [-2** **31-1].

88 Implementacja typu zadaniowego Silnia jest następująca: package body Pakiet_silnia is task body Silnia is Odp: Long_Integer := 1; begin for i in 2.. X loop Odp := Odp * Long_Integer(i); end loop; accept Wynik (Wy: out Long_Integer) do -- zwróć wynik Wy := Odp; end Wynik; end Silnia; end Pakiet_silnia; Przykład 2: Obiekty chronione można wykorzystać do synchronizacji i komunikacji zadań o różnych poziomach abstrakcji. Jednym z nich jest synchronizacja pary zadań przy użyciu sygnałów trwałych (ang. persistent signals}. Jedno z zadań czeka na możliwość kontynuacji tak długo, aż odbierze sygnał synchronizacji od drugiego zadania. Sygnał jest uważany za trwały, gdyż zadanie sygnalizujące może go nadać wcześniej niż zadanie partnerskie zgłosi gotowość do jego odbioru. Implementacja sygnałów trwałych jest oparta na obiekcie chronionym, który przechowuje informacje o oczekiwaniu na sygnał i nadaniu sygnału synchronizacyjnego. Ma ona następującą postać:

89 package Sygnały is protected type Sygnał is procedure Nadaj; entry Czekam; private Sygnal_nadszedl: Boolean ; end Sygnał; end Sygnały; package body Sygnały is protected body Sygnał is procedure Nadaj is begin Sygnal_nadszedl : = True; end Nadaj; entry Czekam when Sygnal_nadszedl is begin Sygnal_nadszedl := Fałse; end Czekaj; end Sygnał; end Sygnały; Nadanie lub odebranie sygnału jest realizowane odpowiednio przez wywołanie procedury Nadaj lub wywołanie wejścia Czekam obiektu chronionego typu Sygnał.

90 Mechanizmy programowania systemów czasu rzeczywistego System czasu rzeczywistego to system, w którym obliczenia są przeprowadzane równolegle z procesem zewnętrznym (otoczeniem) i mają na celu nadzorowanie, sterowanie lub terminowe reagowanie na zdarzenia zachodzące w tym procesie. Cechą charakterystyczną systemów czasu rzeczywistego jest ścisłe sprzężenie między procesem zewnętrznym (otoczeniem), w którym zachodzą zdarzenia, a systemem, który winien zdarzenia te rozpoznawać i reagować na nie w ograniczonym czasie (czas reakcji), określonym przez dynamikę tego otoczenia. Dynamika (szybkość zmian zachodzących w otoczeniu) może określać ostre (ang. hard] lub łagodne (ang. soft) wymagania względem zapewnienia odpowiedniego czasu reakcji. Wiąże się to zazwyczaj z dwoma klasami zastosowań systemów czasu rzeczywistego: systemy sterowania lub dowodzenia (wymagania ostre) oraz systemy komercyjne i biurowe, np. obsługa banków, systemy rezerwacji miejsc (wymagania łagodne). Konstruowanie programu czasu rzeczywistego wymaga rozwiązania dwóch problemów. Problem pierwszy to ułożenie odpowiedniego algorytmu reagowania na obsługiwane zdarzenia. Do zapisania takiego algorytmu wystarcza w zasadzie dowolny język programowania współbieżnego. Współbieżność jest tu istotna, gdyż wielozadaniowość jest naturalnym mechanizmem strukturalizacji programów czasu rzeczywistego daje ona możliwość podziału programu na równolegle wykonywane zadania, które są odpowiedzialne za pojedyncze aktywności. Język musi mieć odpowiednie mechanizmy do komunikacji zadań i powinien mieć dodatkowe mechanizmy do reprezentowania zdarzeń zewnętrznych i reagowania na nie. Wymienione mechanizmy są dostarczane przez jądro języka (zadania, komunikacja synchroniczna i asynchroniczna) i konstrukcje opisane w aneksie C (obsługa przerwań, identyfikatory i atrybuty przerwań, itp.).

91 Drugi problem polega na ułożeniu algorytmu sterowania wykonywaniem programu, w konkretnym środowisku wykonawczym, który zapewni odpowiednią efektywność programu, czyli spełnianie przez zadania programu narzuconych ograniczeń czasowych. Do realizacji tego celu potrzebne są dodatkowe mechanizmy odpowiedniego szeregowania zadań i związane z nimi mechanizmy odmierzania czasu i reakcji na zdarzenia przy ograniczeniach czasowych. Część mechanizmów związana z drugim problemem jest dostarczana przez mechanizmy jądra języka, ale większość jest opisana w aneksie D, o nazwie Systemy czasu rzeczywistego ( ang. Real-Time Systems, RTS}. Podstawowe zagadnienia opisane w aneksie to: - priorytety zadań i ich zmiany w wyniku komunikacji międzyzadaniowej lub interakcji zadań z obiektami chronionymi, - strategie szeregowania zadań, obsługa wejść zadań i obiektów chronionych, - uproszczenia modelu zadań ograniczające nie determinizm programów lub pozwalające na uproszczenia adowego środowiska wykonawczego, - dodatkowe mechanizmy synchronicznego i asynchronicznego sterowania wykonaniem zadań, - zegar monotoniczny o dużej rozdzielczości i dokładności. Wyżej wymienione zagadnienia są opisane w kolejnych punktach niniejszego rozdziału.

92 Priorytety zadań Przykładem metody szeregowania opartej na priorytetach dynamicznych jest metoda EDF (ang. Earliest Deadline First), gdzie priorytet zadania jest funkcją zbliżającego się momentu uruchomienia zadania i wykonania nadzorowanej przez zadanie akcji. Zadanie, któremu do momentu krytycznego pozostaje najkrótszy odcinek czasu, uzyskuje najwyższy priorytet. Priorytet zadań musi być zmieniany dynamicznie i mechanizmy dynamicznej zmiany priorytetu są dostępne dopiero w Adzie 95. Priorytety bazowe Zadaniom nadaje się priorytety statyczne przez użycie odpowiednich pragm, umieszczanych w specyfikacji zadań lub typów zadaniowych. Wartościami priorytetów są liczby całkowite o zakresie zdefiniowanym przez implementację. subtype Any_Priority is Integer range -- zdefiniowany_przez__implementację; subtype Priority is Any_Priority range Any_Priority first.. -- zdefiniowany_przez__implementację; subtype Interrupt_Priority is Any_Priority range Priority'Last + l.. Any_Priority'Last; Default_Priority: constant Priority :=(Priority'First + Priority'Last)/2;

93 Przykład: task Z is pragma Priority(8); end Z; Typowi zadaniowemu T z nadaje się priorytet: task type TZ(Task_Priority: System.Priority) is entry W (...); pragma Priority (Task_Priority); end TZ; Jeżeli przy kreacji zadań o typie T Z nie używa się wyróżnika, to wszystkie utworzone zadania otrzymują ten sam priorytet. Problem inwersji priorytetów Podstawową własnością obiektów chronionych jest wzajemne wykluczanie dostępu do danych enkapsulowanych w obiekcie chronionym. Gdy z obiektami komunikują się zadania o różnych priorytetach, może wystąpić niepożądana inwersja priorytetów tych zadań. Inwersja priorytetów ma miejsce wtedy, gdy zadanie o niższym priorytecie wstrzymuje wykonywanie zadania o wyższym priorytecie.

94 Rozpatrzmy przykład programu z trzema zadaniami W, S, N, mającymi odpowiednio wysoki, średni i niski priorytet. Przyjmijmy, że zadania W i N mają dostęp do danych współdzielonych chronionych przez obiekt chroniony P. Zakładając, że program wykonuje się w systemie jednoprocesorowym, możliwa jest następująca sekwencja zdarzeń: - Zadanie N zostaje zwolnione do wykonania i wykonując się wchodzi do P. - Zadanie S zostaje zwolnione do wykonania i wywłaszcza N (gdy N jest wewnątrz P). - Zadanie W zostaje zwolnione do wykonania i wywłaszcza S. - Zadanie W wywołuje wejście do P. Zadanie W nie może wejść do P, ponieważ zadanie N ma w danej chwili prawo wyłącznego dostępu do obiektu chronionego. Zatem w zostaje zawieszone i wykonuje się zadanie o kolejno najwyższym priorytecie, czyli S. W efekcie w musi czekać, aż zakończy się S. Istnieje kilka metod dziedziczenia priorytetu. Aneks RTS, za pomocą pragmy: pragma Locking_Policy(Ceiling_Locking) udostępnia metodę ICPP (ang. Immediate Ceiling Pńority Protocol). Polega ona na tym, że każdemu obiektowi chronionemu P nadaje się pewien priorytet X, zwany granicznym, równy maksimum z priorytetów zadań komunikujących się z obiektem P. Jeżeli zadanie wchodzi do obiektu chronionego, to na czas interakcji z P dziedziczy ono priorytet X.

95 Szeregowanie wejść Programista może definiować sposób obsługi wywołań wejść zadań i obiektów chronionych, a także sposób wyboru gałęzi w instrukcji select. Jak poprzednio, odpowiedni sposób określa się przez pragmę: pragma Queuing_Policy (Policy__Identifier) ; Z podaną pragma są związane dwie standardowe metody: Fifo_Queuing oraz Prio­rity_Queuing. Implementacja może definiować jeszcze inne metody. Domyślnie, w przypadku braku pragmy, stosuje się metodę Fifo_Queuing. Metoda umieszcza wszystkie wywołujące zadania w jednej kolejce i wybiera z niej do realizacji pierwsze zadanie, dla którego żądana usługa może być wykonana. Metoda Priority_Queuing kolejkuje zadania według ich priorytetów. W obrębie instrukcji select i obiektów chronionych bierze się pod uwagę wejścia z otwartych gałęzi i wybiera do obsługi, spośród czekających zadań, zadanie o najwyższym priory­tecie. Jeżeli na początku kolejek czekają zadania o jednakowym priorytecie, wówczas do obsługi jest wybierane wejście, które w programie jest tekstowo wcześniejsze.

96 Priorytety dynamiczne Często występują sytuacje, w których założenie stałości priorytetów nie jest właściwe. Przykładem mogą być: - potrzeba zmiany priorytetu zadania na skutek zmiany trybu pracy programu (z normalnego na awaryjny i skrócenie czasu reakcji na zachodzące zdarzenia), - szeregowanie zadań według strategii EDF. W takich przypadkach niezbędne stają się zmiany priorytetów bazowych zadań. W celu przeprowadzania takich zmian zdefiniowano pakiet biblioteczny Ada. Dynamic_Prio-rities. Ma on następującą specyfikację: with Ada.Task_Identification; with System; package Ada.Dynamic_Priorities is procedure Set_Priority (Priority: System.Any_Priority; T: Ada.Task_Identification.Task_Id := Ada.Task_Identification.Current_Task); -- generuje wyjątek Tasking_Error, gdy wskazano zadanie nie istniejące -- lub zakończone function Get__Priority (T: Ada. Task_Identif ication. Task_Id : = Ada.Task_Identification.Current_Task) return System.Any_Priority; -- generuje wyjątek Tasking_Error, gdy wskazano zadanie nie istniejące -- lub zakończone private określone przez implementację end Ada.Dynamic_Priorities ;

97 Mechanizmy programowania systemów rozproszonych Program rozproszony składa się z pewnej liczby partycji, które mogą wykonywać się współbieżnie na jednej lub na wielu maszynach połączonych siecią komputerową. Partycja jest zbiorem jednostek kompilacyjnych, połączonych w pewną całość w procesie konsolidacji. Program rozproszony składa się z co najmniej dwóch komunikujących się partycji. Model systemu rozproszonego Wyróżnia się partycje aktywne i pasywne. Partycje aktywne rezydują i wykonują się w węzłach przetwarzających systemu rozproszonego. Partycje pasywne rezydują w węzłach-składnicach systemu rozproszonego. Partycja aktywna może wywołać podprogram w innej partycji aktywnej. Takie wywołania są dopuszczalne tylko wtedy, gdy wywoływany podprogram jest deklarowany w jednostce bibliotecznej kategorii Remote_Call_lnterface. Każda wywołująca partycja, w swojej klauzuli kontekstowej with, musi zawierać nazwę jednostki bibliotecznej, w której umieszczono specyfikację wywoływanego podprogramu. Ilustruje to następujący szkielet programu. package A is pragma Remote_Call procedure P (...) -- partycja aktywna udostępniająca Interface(A); end A; -- podprogramy do zdalnych wywołań with A; -- partycja aktywna, w której występują package B is -- wywołania procedur partycji A A. P ; -- przykładowe wywołanie zdalne end B;

98 Wywołanie podprogramu jednej partycji aktywnej w drugiej partycji aktywnej jest nazywane zdalnym wywołaniem procedury (ang. Remote Procedurę Cali, RPC). W przypadku wywołań RPC kompilator i konsolidator uzupełnia kod programu o tzw. korpusy wywołań (ang. stubs). Korpusy wywołań realizują zdalną komunikację w partycji wywołującej i wywoływanej. Domyślnie, wywołania RPC są synchroniczne. Dodatkowo, pod pewnymi warunkami, wywołanie RPC może być realizowane asynchronicznie. Oznacza, to, że program w partycji wywołującej wznawia aktywność natychmiast po wywołaniu procedury zdalnej i nie czeka na realizację wywołania, jak to ma miejsce przy komunikacji synchronicznej. Implementacja może dostarczać dodatkowe mechanizmy komunikacji. Przykład Zbudowany w ramach projektu GNAT kompilator języka Ada 95 umożliwia budowanie programów rozproszonych. Podany niżej przykład programu rozproszonego zbudowano w oparciu o język konfigurowania partycji, podsystem komunikacji i narzędzia programowe tego właśnie systemu.

99 Rysunek Powiązania komponentów systemu rozproszonego w Adzie Aplikacje typu klient Korpusy Aplikacji klientów System RPC Podsieć komunikacyjna System RPC Aplikacje typu serwer Korpusy serwerów Aplikacje zabudowane Wygenerowane przez kompilator Dostarczone przez implementację

100 Program rozproszony wyprowadza komunikat witaj świecie. Działa on według schematu klient-serwer. W przeciwieństwie do klasycznej wersji wyprowadzającej analogiczny napis, procedura Witaj pobiera tekst do wyprowadzenia ze zdalnego serwera, o nazwie Serwer_Komunikatow. Tekst programu rozproszonego jest zapisany w kilku plikach. Kompilacja, podział na partycje i alokacja partycji są wykonywane w wyniku polecenia gnatdist, które pobiera parametry z pliku konfiguracyjnego ww. cgf. with Serwer_Komunikatow; -- plik: witaj. adb with Text_IO; procedure Witaj is begin Text_IO.Put_line (Serwer_Komunikatow.Komunikat_l); end Witaj; package Serwer_Komunikatow is -- plik: serwer_komunikatow. ads pragma Remote_Call_Interface; function Komunikat_l return String; end Serwer_Komunikatow; package body Serwer_Komunikatow is -- plik: serwer_komunikatow. adb function Komunikat_l return String is begin return "Witaj świecie !"; end Komunikat 1; end Serwer_Komunikatow; configuration Witaj is Klient: Partition := (); procedure Witaj is in Klient; -- plik: ww. cfg Serwer: Partition := (Serwer_Komunikatow); -- zapisany z wykorzystaniem języka end Witaj;

101 Serwer jest implementowany w postaci pakietu. Procedura witaj wywołuje funkcje tego pakietu w ten sam sposób, jak to ma miejsce w programach nie rozproszonych. Jedyna różnica w budowie pakietu, to konieczność zastosowania pragmy Remote_ Call_lnterface, która wskazuje, że funkcje tego pakietu mogą być wywoływane zdalnie. Zakładając, że wszystkie podane pliki są w tej samej kartotece, polecenie: gnatdist ww powoduje kompilację całego programu wraz z automatyczną generacją korpusów wywołań zdalnych, podziału na partycje, itd. Załadowanie i uruchomienie programu następuje po poleceniu Witaj. Programowanie rozproszone Programowanie rozproszone można inaczej nazwać przetwarzanie rozproszone. Wysyłanie komunikatu jest akcją wyższego poziomu, która może być łatwo zaimplementowana na fizycznie rozproszonych procesorach.

102 Algorytmy - Pozwolenie ALGORYTM CENTRALNEGO SERWERA Stworzenie serwera koordynującego (udzielającego pozwoleń) wejście procesów do sekcji krytycznej jest jedną z podstawowych i najprostszych metod realizacji wzajemnego wykluczania w środowisku rozproszonym. Sposób działania takiego algorytmu przy założeniu, że istnieje tylko jedna sekcja krytyczna wydaje się mało skomplikowany i prosty do realizacji. Przy takich założeniach, proces, który chce wejść do zasobu dzielonego wysyła do serwera komunikat z zamówieniem i oczekuje na odpowiedź, często porównywaną do otrzymania żetonu dającego prawo do wejścia. W sytuacji gdy sekcja krytyczna jest pusta serwer natychmiast "wręcza" procesowi żeton, a proces może wejść do sekcji. Jeżeli żeton jest w posiadaniu innego procesu (żeton jest tylko jeden - spełnienie warunku, że w sekcji może w danej chwili znajdować się tylko jeden proces), wówczas proces zamawiający jest ustawiany w kolejce serwera. Wychodząc z sekcji krytycznej każdy z procesów zwraca żeton serwerowi, wysyłając jednocześnie komunikat o opuszczeniu sekcji. W przypadku gdy serwer udostępnia wejście do sekcji procesom znajdującym się w kolejce (w danej chwili tylko pierwszemu z nich), wówczas kieruje się priorytetem najstarszego wpisu. Pozwolenie na wejście uzyskuje proces, który ustawi się w kolejce najwcześniej. Poniższe rysunki obrazują kolejne kroki działania tego algorytmu dla czterech procesów. Na rysunku a) widać sytuację, w której w sekcji krytycznej znajduje się proces P3 (jest w posiadaniu żetonu), proces P4 wysłał już zamówienie żetonu do serwera wcześniej, natomiast proces P2 wysyła właśnie zamówienie na żeton. Kolejny krok algorytmu pokazuje zachowanie się poszczególnych procesów oraz żetonu. Proces P2 ustawił się w kolejce lokalnej serwera, żeton został zwrócony i natychmiast zostanie przekazany procesowi zamawiającemu najwcześniej, tzn. procesowi P4 (rys. b). Na rysunku c) proces P4 znajduje się w sekcji, na początku kolejki znalazł się proces P2, a kolejne procesy mogą wysyłać zamówienia.

103

104 Algorytm centralnego serwera. Oczywiście algorytm ten spełnia warunki wymienione jako warunki wzajemnego wykluczania. Jednak system z zastosowaniem pojedynczego serwera jest podatny na awarie bardzo brzemienne w skutkach. Ze względu na to, że przez serwer przechodzą wszystkie operacje, jego uszkodzenie może być bardzo niebezpieczne dla całego systemu. Z tego powodu opisany algorytm jest jedynie obrazem przedstawiającym podstawy realizacji wzajemnego wykluczania i nie jest stosowany w rozwiązaniach systemów rozproszonych. ALGORYTM LAMPORTA Pierwszym algorytmem rozwiązującym problem wzajemnego wykluczania w środowisku rozproszonym jest algorytm Lamporta. Podobnie jak algorytm centralnego serwera spełnia warunki bezpieczeństwa, ruchu i uporządkowania. Algorytm zakłada, że wszystkie procesy w systemie posiadają lokalną kolejkę przechowującą komunikaty żądania innych procesów oraz znacznik czasu. Wysyłając komunikat, każdy proces zaopatruje go w numer dający informację, z którego procesu wysłano komunikat oraz w znacznik czasu w celu właściwego ich odbioru i interpretacji przy udzielaniu pozwolenia wejścia do sekcji. proces, który chce wejść do sekcji krytycznej, tworzy komunikat żądania, który oprócz tego, że jest wysyłany do innych procesów, jest także umieszczany w kolejce procesu wysyłającego żądanie. Komunikat taki ma swój znacznik czasu. Proces odbierający żądanie natychmiast wysyła odpowiedź ze znacznikiem czasu, a przybyłe żądanie umieszcza w swojej kolejce. Proces wysyłający żądanie będzie mógł wejść do sekcji krytycznej jeżeli jego komunikat będzie znajdował się na początku kolejki procesu wysyłającego oraz gdy jego komunikat zostanie odebrany przez inne procesy, a otrzymana odpowiedź będzie miała większy znacznik czasu od znacznika czasu żądania. Po opuszczeniu sekcji krytycznej proces usuwa własny numer z kolejki i zawiadamia wszystkie inne procesy w systemie o zwolnieniu zasobu dzielonego.

105 Wówczas procesy otrzymujące komunikat zwalniający "czyszczą" własną kolejkę z numerem procesu opuszczającego sekcję. Z powyższego opisu wynika, że najistotniejszymi założeniami algorytmu Lamporta są: utrzymanie lokalnej kolejki przez wszystkie procesy oraz komunikacja między wszystkimi procesami. Stosowanie tego rozwiązania jest jednak ograniczone ze względu na możliwość zbyt dużej liczby komunikatów w systemie (przy dużej liczbie procesów) i konieczność ich wymiany co powoduje powstawanie lokalnych kolejek o ogromnych rozmiarach. Algorytm Lamporta, przy założeniu, że w systemie jest N procesów, wymaga wymiany 3(N-1) komunikatów. Tak więc przy 3 procesach konieczna jest wymiana 6 komunikatów

106 Przykład działania algorytmu Lamporta. Na powyższym rysunku przedstawiono przykładowy schemat działania algorytmu Lamporta w przypadku istnienia w systemie trzech procesów zainteresowanych wejściem do zasobu dzielonego. W sytuacji a) widać, że każdy z procesów wysyła zamówienie ze znacznikiem czasu do każdego z procesów w systemie, a swój znacznik ustawia w lokalnej kolejce. Następnie dokonywana jest klasyfikacja zamówień od wszystkich procesów i w lokalnej kolejce są ustawiane poszczególne zamówienia, a proces z najmniejszym znacznikiem uzyskuje dostęp do sekcji krytycznej po otrzymaniu odpowiedzi od innych procesów. Odpowiedzi procesów są wysyłane natychmiast po otrzymaniu komunikatów zamówień i po stwierdzeniu przez proces P3, że mają one znaczniki czasu późniejsze oraz to, że jego identyfikator jest umieszczony na początku lokalnej kolejki wchodzi on do sekcji krytycznej. W momencie opuszczania przez proces zasobu dzielonego wysyła komunikaty zwalniające do pozostałych procesów w celu umożliwienia im wejścia do sekcji. W powyższym przykładzie widać, że kolejnym procesem, który wejdzie do sekcji będzie proces, który znajduje się najwyżej w kolejkach lokalnych tj. proces P1. Zasada postępowania jest dla tego procesu jest identyczna.

107 ALGORYTM RICARTA I AGRAWALI Kolejnym rozwiązaniem realizacji wzajemnego wykluczania jest opracowany przez Ricarta i Agrawalę algorytm oparty na rozproszonym uzgadnianiu zwany często jako algorytm z zastosowaniem zegarów logicznych. Podobnie jak w algorytmie Lamporta, również w tym rozwiązaniu każdy proces chcący wejść do sekcji krytycznej rozsyła komunikat do wszystkich procesów w systemie i może wejść do sekcji dopiero wówczas, gdy dostanie odpowiedź od pozostałych komunikatów. Założenia tego algorytmu są następujące: - istnieje jedna sekcja krytyczna (dla ułatwienia), - wszystkie procesy znają wzajemnie swoje adresy, - wszystkie komunikaty zostaną w końcu dostarczone, - każdy proces utrzymuje zegar logiczny (znacznik czasu) Można zauważyć, że algorytm ten jest modyfikacją rozwiązania zaproponowanego przez Lamporta. Procesy wysyłają komunikaty, które posiadają znacznik czasu T i identyfikator nadawcy p (T, p). Proces, który otrzymał od innego procesu komunikat zamawiający wejście do sekcji działa następująco: 1) jeżeli sam wcześniej nie wysłał prośby (nie czeka na dostęp), to odpowiada pozytywnie 2) jeżeli sam wcześniej wysłał prośbę i jeszcze nie uzyskał wszystkich odpowiedzi, to o tym kto pierwszy wykona sekcje krytyczna decyduje czas wysłania próśb:

108 a.jeżeli zapytywany proces wysłał swoja prośbę później, to także odpowiada pozytywnie b.jeżeli prośby zostały wysłane w tym samym momencie, to o kolejności decydują priorytety (np. jeżeli zapytany proces ma wyższy numer to, odpowiada pozytywnie) c.w każdym innym przypadku zapytywany proces wstrzymuje się z odpowiedzią, aż do chwili, gdy sam skończy wykonywać swoja sekcje krytyczna - wówczas odpowiada pozytywnie wszystkim, którym jeszcze nie odpowiedział. Wejście do sekcji krytycznej może nastąpić tylko wówczas gdy proces wysyłający żądanie otrzyma od każdego innego procesu odpowiedź. Gdy proces wykonał swoje zadania w sekcji krytycznej może ją opuścić informując jednocześnie o swym kroku procesy których żądania przetrzymuje w lokalnej kolejce. W porównaniu z algorytmem Lamporta wprowadzono tutaj kolejkę logiczną opierającą się o wartości 0 i 1 oraz zrezygnowano z komunikatu zwalniającego. Dzięki zastosowaniu tych zmian zmalała liczba komunikatów niezbędnych do wejścia do sekcji krytycznej z 3(N-1) w algorytmie Lamporta do 2(N-1).

109 Przykład działania algorytmu Ricarta i Agrawali. Powyższa ilustracja przedstawia sytuację, w której mamy do czynienia z trzema procesami, z których współbieżne zamówienie wejścia do sekcji wysyłają procesy P1 i P2. Oba zamawiające procesy mają znaczniki czasu odpowiednio P1- 41, a P Proces P3 w przypadku otrzymania zamówienia odpowie natychmiast (nie jest zainteresowany wejściem do zasobu dzielonego w danej chwili). Inaczej sytuacja przedstawia się w przypadku procesów zainteresowanych wejściem do sekcji. Proces P2 odbierając komunikat zamówienie od procesu P1 stwierdzi, że jego własne zamówienie ma znacznik czasu mniejszy niż zamówienie procesu P1. Z tego powodu nie udzieli odpowiedzi. Natomiast proces P1 otrzymując komunikat od procesu P2 z mniejszym znacznikiem czasu odpowie natychmiast umożliwiając wejście do sekcji procesowi P2. W momencie opuszczenia sekcji krytycznej przez proces P2 wysyła on odpowiedź do procesu P1 umożliwiając mu wejście do sekcji. Wprawdzie jest to algorytm rozproszony, jednak awaria dowolnego procesu uniemożliwi działanie algorytmu. Kolejną wadą jest to, że wszystkie procesy przetwarzają wszystkie zamówienia co osłabia wydajność systemu.

110 ALGORYTM MAEKAWY Ciekawe podejście do problemu wzajemnego wykluczania przedstawił w swoim rozwiązaniu Maekawa. Mając do dyspozycji N procesów w systemie Maekawa podzielił go na podzbiory z uwzględnieniem pewnych warunków. Pierwszy warunek zwany "regułą równego wysiłku", mówi, że podział systemu na podzbiory odbywa się w ten sposób, że każdy podzbiór ma taki sam rozmiar. "Reguła niepustego przejęcia" oznacza, że dla dowolnej pary podzbiorów istnieje element, który należy do każdego z nich. "Reguła równej odpowiedzialności" oznacza, że każdy proces zawiera się dokładnie w takiej samej liczbie podzbiorów. Ostatni warunek oznacza, że każdy proces zawiera się we własnym podzbiorze i zwany jest "zawieraniem się we własnym podzbiorze". Sposób działania algorytmu jest nieco bardziej skomplikowany ze względu na dość dużą liczbę komunikatów. Proces, który zamierza wejść do sekcji krytycznej wysyła komunikat żądanie do wszystkich procesów z własnego podzbioru. Komunikat ten jest zaopatrzony w numer kolejności (lub znacznik czasu) większy niż numery kolejności odebrane wcześniej przez ten proces lub wcześniej zauważone przez proces w systemie. Każdy z procesów w podzbiorze otrzymujących komunikat żądanie wysyła następnie do procesu chcącego wejść do sekcji krytycznej komunikat blokada, a wcześniej zaznacza siebie jako zablokowany, przy uwzględnieniu warunku, że wcześniej nie został zablokowany. Jeżeli dojdzie do sytuacji, że proces był już wcześniej zablokowany wówczas komunikat żądanie jest umieszczany w lokalnej kolejce, a do procesu chcącego korzystać z zasobu wysyłany jest komunikat niepowodzenie. W przypadku gdy komunikat żądania ma niższy numer niż komunikat już blokujący proces w podzbiorze wówczas wysyłany jest komunikat pytanie.

111 Proces otrzymujący komunikat pytanie kasuje komunikat blokowanie (jeżeli w kolejce jest komunikat niepowodzenie) i zwraca komunikat opuszczenie. Po odebraniu komunikatu pytanie i opuszczeniu sekcji krytycznej proces wysyła odpowiedź. Proces, który otrzymał komunikat opuszczenie blokuje się dla komunikatu żądania z najmniejszym numerem kolejności. Jeżeli zaistnieje sytuacja w której wszystkie procesy z podzbioru wysłały komunikat blokowanie to proces wysyłający żądanie może wejść do sekcji krytycznej. Opuszczając sekcję proces wysyła do wszystkich procesów danego podzbioru komunikat odpowiedź. proces otrzymujący komunikat odpowiedź usuwa żądanie blokujące proces z lokalnej kolejki i znowu przechodzi w stan blokowania dla procesu, który wysłał komunikat żądanie z najmniejszym numerem kolejności. Poniższa ilustracja przedstawia przykładowy sposób działania tego algorytmu, przy założeniu, że w systemie występuje sześć procesów:

112 Zasada działania algorytmu Maekawy. System składa się z sześciu procesów, z których proces P1 chce wejść do sekcji krytycznej. Wysyła zatem komunikaty żądania ze znacznikiem do procesów własnego podzbioru tj. P2 i P3 oraz do samego siebie. Procesy otrzymujące komunikat żądanie wysyłają komunikaty blokowanie do procesu P1, który może wejść do sekcji krytycznej. W czasie gdy P1 przebywa i korzysta z zasobu dzielonego do sekcji krytycznej chce wejść proces z innego podzbioru tj. P6 i wysyła żądania do wszystkich procesów ze swojego podzbioru w tym także do procesu P2 (oraz do P4). Proces P4 natychmiast zwraca komunikat blokowanie natomiast P2 musi ustawić żądanie w kolejce, gdyż jest już zablokowany przez proces P1 (aktualnie korzystający z zasobu). Proces P2 jest w tym momencie swoistym arbitrem. W momencie opuszczenia sekcji przez proces P1 otrzyma on odpowiedź i wówczas będzie mógł wysłać komunikat blokowanie do procesu P6, który w ten sposób będzie mógł wejść do sekcji.

113 ALGORYTM LE LANNA - żeton Jednym z prostszych algorytmów opartych na zasadzie żetonu jest algorytm podany przez Le Lanna. Realizacja wzajemnego wykluczania pomiędzy procesami może odbywać się w oparciu o pierścień logiczny zbudowany z procesów znajdujących się w systemie. Budowa takiego pierścienia jest bardzo prosta. Każdy proces ma swój adres, a komunikaty są przekazywane w jednym ustalonym kierunku, wokół całego pierścienia. Żeton jest uzyskiwany razem z komunikatem i zachowuje się jak kulka ruletki z tą różnicą, że zatrzymuje się ("odwiedza") przy każdym procesie w pierścieniu. Każdy proces w systemie zna adres swojego sąsiada w pierścieniu. Sposób wejścia do sekcji jest mało skomplikowany i polega na otrzymaniu przez zainteresowany wejściem proces żetonu wraz z komunikatem. Na początku działania algorytmu należy przyjąć, który proces będzie posiadał żeton. Jako kryterium można tu przyjąć numer procesu (najmniejszy lub największy). Proces otrzymując żeton od sąsiada wchodzi do sekcji (jeżeli tego żądał), a wychodząc z sekcji przekazuje go do sąsiada zgodnie z kierunkiem poruszania się komunikatów w pierścieniu. Jeżeli proces nie był zainteresowany korzystaniem z zasobu dzielonego, przekazuje żeton natychmiast po jego otrzymaniu. W sytuacji w której żaden z procesów nie jest zainteresowany korzystaniem z zasobu żeton krąży bez przerwy w pierścieniu.

114 Przyjęcie zasady poruszania się żetonu w jednym kierunku daje gwarancje bezpieczeństwa i żywotności. W rozwiązaniu tym nie ma jednak pełnienia zasady uprzedniości, tzn. zasady mówiącej, że pozwolenie na wejście do sekcji otrzymuje proces zgłaszający żądanie najwcześniej. Sytuacja w której żaden z procesów nie wchodzi do sekcji krytycznej jest niekorzystna ze względu na to, że bez przerwy krążący żeton zajmuje jeden z kanałów komunikacyjnych. Również awaria jednego z procesów w systemie może spowodować awarię całego systemu. Naprawa takiej usterki polega wówczas na usunięciu uszkodzonego procesu z systemu. Może zaistnieć sytuacja, że uszkodzeniu ulegnie proces, który w danej chwili przechowywał żeton. Wówczas po upewnieniu się, że proces rzeczywiście uległ awarii zwołuje się elekcję, która wybiera proces do regeneracji żetonu.

115 ALGORYTM SUZUKI I KASAMI Algorytm Suzuki i Kasami jest rozwiązaniem eliminującym wadę algorytmu zaproponowanego przez Le Lanna. Jego główna idea polega na tym, że przekazuje żeton oraz wykorzystuje komunikaty żądania przesyłane do procesów w systemie, w ten sposób, że żeton może być przekazywany bezpośrednio do procesu wysyłającego komunikaty. Tak więc wykluczone jest tu przekazywanie żetonu gdy w systemie nie ma procesu, który w danej chwili chce wejść do sekcji krytycznej. Proces chcący korzystać z zasobu dzielonego przy założeniu, że nie posiada w danej chwili żetonu wysyła do pozostałych procesów w systemie komunikat żądanie. Proces będący w tym czasie w posiadaniu żetonu natychmiast przesyła go do procesu wysyłającego komunikat żądanie. Wyjątkiem jest sytuacja, w której proces z żetonem wykonuje właśnie sekcję krytyczną (tzn. korzysta w danej chwili z zasobu dzielonego). Wówczas przekazanie żetonu nastąpi po zakończeniu wykonywania operacji na zasobie dzielonym przez proces obecnie korzystający z zasobu. Jeżeli żaden proces nie żąda wejścia do sekcji to żeton "stoi w miejscu" i proces posiadający żeton może wykonywać sekcję wielokrotnie. Aby algorytm ten mógł działać poprawnie procesy i żeton zawierają dodatkowe informacje. Żeton zawiera zatem wektor, w którym na kolejnych pozycjach znajdują się numery obsłużonych żądań (na pierwszej pozycji znajduje się numer kolejności ostatnio obsłużonego żądania). Oprócz tego żeton zawiera również kolejkę żądań procesów wraz z ich identyfikatorami. Również procesy przechowują w swojej pamięci lokalnej wektor żądań z największym numerem kolejności otrzymanym do chwili wysłania w komunikacie żądania oraz własny numer kolejności. Jeżeli proces chce wejść do sekcji i nie jest w posiadaniu żetonu zwiększa jednostkowo własny numer kolejności oraz numer kolejności w wektorze żądań. Komunikat żądania wysyłany jest już z nową wartością własnego numeru. Procesy odbierające komunikat żądania porównują wartości własne procesu wysyłającego z wartością w wektorze żądań. Jeżeli wyniki porównań dadzą informacje, że komunikat nie jest "przeterminowany" oraz, że żądanie nie zostało wcześniej obsłużone to żeton zostanie przekazany. Proces opuszczający sekcję i wysyłający żeton zapisuje w wektorze żetonu, że żądanie zostało obsłużone

116 Schemat przedstawiający przekazanie żetonu w algorytmie Suzuki i Kasami. Na rysunku a) proces P3 jest w posiadaniu żetonu. Następnie chęć korzystania z sekcji krytycznej wyraża proces P4 i wysyła komunikaty żądania do wszystkich procesów w systemie z zachowaniem zasady zwiększenia wartości przechowywanych w pamięci. procesy w systemie odbierając taki komunikat modyfikują odpowiednio własne wektory żądań. Następnie porównywane są wartości poszczególnych wektorów i proces zostaje przesłany do procesu, który wysłał komunikat żądanie (po uprzednim upewnieniu się, że żądanie to nie zostało jeszcze obsłużone).

117 ALGORYTM NAIMI, TREHEL I ARNOLDA Podobnie jak w rozwiązaniu z zastosowaniem algorytmu centralnego serwera również w algorytmie Naimi, Trehel i Arnolda proces chcący wejść do sekcji krytycznej wysyła swoje żądanie do jednego punktu, a nie do wszystkich procesów w systemie. W algorytmie centralnego serwera żądanie było wysyłane do centralnego punktu - serwera. Natomiast w algorytmie zaproponowanym przez Naimi, Trehel i Arnolda, żądanie jest przesyłane do procesu, który się zmienia tzn. nie jest to przez cały czas ten sam proces. Po wysłaniu komunikatu tylko do jednego procesu, proces wysyłający żądanie oczekuje na żeton. W algorytmie tym wszystkie procesy są dodatkowo wyposażone w zmienne zawierające informacje pomocnicze, niezbędne do właściwej realizacji i pracy tego algorytmu. Zmienne te zawierają identyfikator następnego w kolejności procesu zgłaszającego chęć wejścia do sekcji (zmienna "następny"), identyfikator procesu, który jest w danej chwili punktem centralnym w systemie i do którego są przesyłane komunikaty żądania (zmienna "ojciec"), logiczny identyfikator dający informację, czy proces posiada w danej chwili żeton czy nie (zmienna "stan obecny") oraz drugi logiczny identyfikator, który "trzyma' wartość 1 (true) w przypadku wysłania komunikatu do momentu zaprzestania korzystania z zasobu (zmienna "żądanie"). Przekazanie żetonu z procesu posiadającego żeton do innego, może nastąpić tylko gdy sam proces (posiadacz żetonu) opuścił sekcję i kolejka nie jest pusta oraz gdy żądanie nadejdzie w chwili gdy nie wykorzystuje żetonu. Proces zamierzający wejść do sekcji krytycznej ustawia wartość zmiennej żądanie na true (wartość 1). Jeżeli równocześnie zmienna ojciec będzie różna od Nil to żądanie zostanie wysłane do procesu wskazywanego przez zmienną ojciec. Wówczas następuje przekierowanie zmiennej ojciec na wartość Nil. Proces, który otrzyma żądanie sprawdza swój stan.

118 Jeżeli jest w stanie oczekiwania na żeton musi sprawdzić wartość zmiennych żądanie i w przypadku gdy wynosi on true to zmienna następny wskazuje na proces inicjujący żądanie. Jeżeli jest ona równa false to proces odbierający żądanie jest w posiadaniu żetonu. Przed wykonaniem tych czynności proces otrzymujący żądanie sprawdza zmienną ojciec. Następnie żeton jest przekazywany do procesu, który wysłał żądanie. Po otrzymaniu żetonu zmienna stan jest równa true, a przy wyjściu z sekcji zmienna żądanie jest równa false.

119 Schemat działania algorytmu Naimi, Trehel i Arnolda W sytuacji przedstawionej na powyższym rysunku proces P1 jest posiadaczem żetonu i znajduje się w sekcji krytycznej. Linia ciągła w tym schemacie reprezentuje zmienną ojciec i wskazuje proces do którego ma być przekazywany komunikat żądanie. Linia przerywana natomiast reprezentuje zmienną następny. Proces P2 zamierzający wejść do sekcji krytycznej wysyła żądanie do aktualnie wskazywanego procesu P1. W momencie opuszczenia sekcji przez proces P1 żeton zostanie przekazany do procesu P2. Jeżeli jako następny będzie chciał wejść proces P4, wysyła żądanie do procesu P1, a natychmiast po tym zdarzeniu proces P1 wyśle żądanie do P2. Wówczas zmienna następny w procesie P2 będzie wskazywała na proces P4, a zmienne ojciec w procesach P1 i P2 na proces P4. W momencie opuszczenia sekcji przez proces P2, żeton zostanie przekazany do procesu P4.


Pobierz ppt "Programowanie rozproszone ADA 95. Wstęp Ada 95 jest uniwersalnym językiem oprogramowania przeznaczonym do tworzenia oprogramowania dużej skali. Rozszerzenie."

Podobne prezentacje


Reklamy Google