Programowanie współbieżne i rozproszone/PWR Ćwiczenia 3: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
| Linia 179: | Linia 179: | ||
: Podaj przeplot prowadzący do zagłodzenia czytelników. | : Podaj przeplot prowadzący do zagłodzenia czytelników. | ||
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź <div class="mw-collapsible-content" style="display:none"> | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
Oto przeplot prowadzący do niepożądanej sytuacji: | <div class="mw-collapsible-content" style="display:none">Oto przeplot prowadzący do niepożądanej sytuacji: | ||
# Zjawia się pisarz ''P1'' i jest wpuszczany do czytelni. | # Zjawia się pisarz ''P1'' i jest wpuszczany do czytelni. | ||
# W trakcie pobytu pisarza w czytelni zjawi się czytelnik ''C1''. Będziemy go głodzić. | # W trakcie pobytu pisarza w czytelni zjawi się czytelnik ''C1''. Będziemy go głodzić. | ||
| Linia 241: | Linia 241: | ||
: Ile buforów jest potrzebnych? | : Ile buforów jest potrzebnych? | ||
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź <div class="mw-collapsible-content" style="display:none"> | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
<div class="mw-collapsible-content" style="display:none"> | |||
Jak zwykle przy rozwiązaniach typu klient-serwer główny ciężar spoczywa na serwerze. Procesy muszą jedynie informować serwer o swoich działaniach i oczekiwać na jego zgodę na nie. Czytelnik przez rozpoczęciem czytania prosi serwer o zgodę. Podobnie postępuje pisarz przed rozpoczęciem pisania. Następnie procesy oczekują na zgodę. Do przekazywania zezwoleń wykorzystamy dwa bufory: osobny dla czytelnika, osobny dla pisarza. Jest to konieczne, gdyż serwer musi mieć możliwość obudzenia jedynie pisarzy lub jedynie czytelników. Procesy przed opuszczeniem czytelni informują o tym serwera. | Jak zwykle przy rozwiązaniach typu klient-serwer główny ciężar spoczywa na serwerze. Procesy muszą jedynie informować serwer o swoich działaniach i oczekiwać na jego zgodę na nie. Czytelnik przez rozpoczęciem czytania prosi serwer o zgodę. Podobnie postępuje pisarz przed rozpoczęciem pisania. Następnie procesy oczekują na zgodę. Do przekazywania zezwoleń wykorzystamy dwa bufory: osobny dla czytelnika, osobny dla pisarza. Jest to konieczne, gdyż serwer musi mieć możliwość obudzenia jedynie pisarzy lub jedynie czytelników. Procesy przed opuszczeniem czytelni informują o tym serwera. | ||
</div></div> | </div></div> | ||
| Linia 313: | Linia 314: | ||
: Zaimplementuj obsługę komunikatu ChcęCzytać. | : Zaimplementuj obsługę komunikatu ChcęCzytać. | ||
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź <div class="mw-collapsible-content" style="display:none"> | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
Serwer po odbiorze komunikatu <tt>ChcęCzytać</tt> powinien: | <div class="mw-collapsible-content" style="display:none"> Serwer po odbiorze komunikatu <tt>ChcęCzytać</tt> powinien: | ||
# Zmodyfikować odpowiednie zmienne | # Zmodyfikować odpowiednie zmienne | ||
# Sprawdzić, czy można dać czytelnikowi zgodę na wejście do czytelni | # Sprawdzić, czy można dać czytelnikowi zgodę na wejście do czytelni | ||
| Linia 335: | Linia 336: | ||
; Ćwiczenie (ukrywajka) | ; Ćwiczenie (ukrywajka) | ||
: Zaimplemetuj ją. | : Zaimplemetuj ją. | ||
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź <div class="mw-collapsible-content" style="display:none"> | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
Jedyna różnica polega na innym warunku oczekiwania: | <div class="mw-collapsible-content" style="display:none">Jedyna różnica polega na innym warunku oczekiwania: | ||
ChcęPisać: | ChcęPisać: | ||
'''begin''' | '''begin''' | ||
Aktualna wersja na dzień 16:02, 2 paź 2006
Zadanie 1. Wzajemne wykluczanie.
Treść
Stosując mechanizm komunikacji asynchronicznej, omówiony na wykładzie, rozwiąż problem wzajemnego wykluczania dla wielu procesów. Co zmieni się w rozwiązaniu, jeśli zezwolimy na pobyt w sekcji krytycznej jednocześnie K>0 procesom?
Rozwiązanie pierwsze
Przedstawione na wykładzie rozwiązanie działa dobrze także wtedy, gdy w systemie działa wiele procesów. Każdy z nich wykonuje kod:
process Pr (i: integer);
var
k: Komunikat;
begin
repeat
własne_sprawy;
GetMessage (b, k)
sekja_krytyczna;
SendMessage (b, k)
until false
end;
Przed uruchomieniem procesów (powiedzmy, że jest ich P) musimy jeszcze zadbać o to, by w buforze b było K komunikatów:
const
P = ...;
K = ...;
type
Komunikat = (Bilet);
var i: integer;
begin
for i := 1 to K do
SendMessage (b, Bilet);
cobegin
for i := 1 to P do Pr(i)
coend
end
Rozwiązanie drugie. Serwer.
Częstym rozwiązaniem stosowanym w modelu rozproszonym jest wprowadzanie dodatkowych procesów, które nadzorują synchronizację pozostałych. Zastosujmy takie podejście do omawianego zadania. Wprowadźmy dodatkowy proces o nazwie Dozorca, który będzie nadzorował dostęp do sekcji krytycznej.
Schemat komunikacji
- Ćwiczenie (ukrywajka)
- O jakich zdarzeniach powinien informować dozorcę proces?
- O czym powinien informować serwer?
- Jakie bufory są potrzebne?
Treść procesu
- Ćwiczenie (ukrywajka)
- Napisz treść procesu
Treść serwera
Oczywiście przy tego typu rozwiązaniach istota rozwiązania tkwi w kodzie serwera, który w nieskończonej pętli odbiera komunikat, po czym go przetwarza. W naszym zadaniu serwer musi pamiętać, ile jest wolnych miejsc w sekcji krytycznej. Gdy przychodzi prośba od procesu i są wolne miejsca, wtedy serwer zgadza się na wejście procesu, wysyłając mu natychmiast zezwolenie; jeśli wolnych miejsc nie ma, to zezwolenie musi zostać wysłane potem.
- Ćwiczenie (ukrywajka)
- Co powinien dodatkowo zapamiętać serwer, gdy nie może od razu udzielić zgody na wejście do sekcji krytycznej?
- Czy wprowadzenie numerów procesów jest rzeczywiście niezbędne?
- Ćwiczenie (ukrywajka)
- Napisz kod serwera
- Test wielokrotnego wyboru
- To rozwiązanie jest
- bezpieczne
- żywotne
- procesy wchodzą do sekcji krytycznej w kolejności odbierania przez serwer zamówień
- serwer może wydać zgodę procesowi oczekującemu, ale skorzysta z niej proces, który nie czekał
To rozwiązanie ma pewną subtelność: procesy nie muszą wchodzić do sekcji krytycznej w kolejności jej zamawiania. Dzieje się tak, gdyż nie precyzujemy, że procesy oczekujące na GetMessage oczekują w kolejce. Może się nawet zdarzyć, że zezwolenie "przeznaczone" dla procesu oczekującego na zgodę odbierze proces, który dopiero co zgłosił zapotrzebowanie. Jednak na mocy sprawiedliwości operacji pobierania z bufora nie dojdzie do zagłodzenia żadnego procesu.
Zadanie 2. Czytelnicy i pisarze
Rozwiąż za pomocą notacji z wykładu problem czytelników i pisarzy.
Rozwiązanie pierwsze
Zanim napiszemy program, zastanówmy się, jaki jest ogólny schemat rozwiązywania tego typu zadań synchronizacyjnych. Mamy tutaj dwie grupy procesów, które muszą w jakiś sposób zsynchronizować korzystanie z "czytelni" --- tak będziemy nazywać wykonanie operacji czytania przez czytelnika lub pisania przez pisarza. Mamy więc następujący schemat działania czytelnika i pisarza:
process Czytelnik;
begin
repeat
własne_sprawy;
protokół_wstępny_czytelnika;
CZYTANIE;
protokół końcowy_czytelnika
until false
end
|
process Pisarz;
begin
repeat
własne_sprawy;
protokół_wstępny_pisarza;
PISANIE;
protokół końcowy_pisarza
until false
end
|
Nie znamy liczby czytelników i pisarzy --- może ich być dowolnie dużo. Podobnie w żaden sposób nie limitujemy liczby miejsc w czytelni.
Z reguły projektując protokoły wstępne musimy odpowiedzieć na pytanie Kiedy proces powinien poczekać. Z kolei pisząc protokoły końcowe musimy budzić procesy uśpione w protokołach wstępnych. Trzeba wtedy często podejmować decyzję, którą grupę procesów obudzić. Spróbujmy zatem odpowiedzieć na te pytania:
- Protokół wstępny czytelnika
- czytelnik czeka, jeśli w czytelni jest pisarz.
- Protokół wstępny pisarza
- pisarz czeka jeśli, w czytelni ktoś jest.
- Protokół końcowy czytelnika
- jeśli jest ostatnim wychodzącym, to wpuszcza pisarza (o ile czeka).
- Protokół końcowy pisarza
- jeśli czeka pisarz, to budzimy go, w przeciwnym razie, jeśli czekają czytelnicy, budzi ich wszystkich.
Zanim zaczniemy implementować to rozwiązanie, odpowiedzmy na kilka pytań:
- Test wielokrotnego wyboru
- To rozwiązanie jest żywotne
- To rozwiązanie jest bezpieczne
- To rozwiązanie prowadzi do zagłodzenia czytelników
- To rozwiązanie prowadzi do zagłodzenia pisarzy
W zależności od rozwoju sytuacji, przedstawione rozwiązanie może zagłodzić pisarzy lub może zagłodzić czytelników.
- Ćwiczenie (ukrywajka)
- Podaj przeplot prowadzący do zagłodzenia czytelników.
Widać zatem, że za zaistniałą sytuację odpowiada protokół końcowy. Spróbujmy go poprawić.
Rozwiązanie drugie
- Protokół wstępny czytelnika
- czytelnik czeka, jeśli w czytelni jest pisarz.
- Protokół wstępny pisarza
- pisarz czeka, jeśli w czytelni ktoś jest.
- Protokół końcowy czytelnika
- jeśli jest ostatnim wychodzącym, to wpuszcza pisarza (o ile czeka).
- Protokół końcowy pisarza
- jeśli czekają czytelnicy, to budzi ich wszystkich, w przeciwnym razie, jeśli czeka pisarz, budzi jednego.
Zmianie uległ protokół końcowy pisarza. Aby nie dopuścić do zagłodzenia w systemie, w którym działają dwie grupy procesów, musimy dbać o to, aby grupy te budziły się naprzemiennie (no chyba, że nikt z drugiej grupy nie czeka). Czy taka poprawka jednak wystarczy?
- Test wielokrotnego wyboru
- Tym razem nie zagłodzimy czytelników.
- Ale wpuścimy jednocześnie czytelnika i pisarza do czytelni.
- Dalej mamy zagłodzenie czytelników
- Dalej mamy zagłodzenie pisarzy
Tym razem uniknęliśmy głodzenia czytelników, ale w dalszym ciągu mamy zagłodzenie pisarzy. Tym razem winien jest protokół wstępny czytelnika. Zezwala on na ciągły napływ nowych czytelników do czytelni, a w konsekwencji może się zdarzyć tak, że czytelnia nigdy nie będzie pusta, bo zawsze będą w niej czytelnicy.
- Ćwiczenie
- Skonstruuj odpowiedni przeplot.
- Zaproponuj poprawkę protokołu wstępnego. Uważaj przy tym, aby znów nie zagłodzić czytelników!
Rozwiązanie trzecie
Poprawne rozwiązanie polega na zatrzymaniu czytelnika także wtedy, gdy czeka pisarz. Choć z pozoru wygląda to na głodzenie czytelników (no bo przecież zatrzymujemy czytelnika, gdy czeka pisarz, a może się zdarzyć, że zawsze będą czekać jacyś pisarze), to jednak uprzednia zmiana protokołu końcowego powoduje, że wychodzący pisarz wpuści wszystkich czytelników, którzy nadeszli podczas jego pobytu w czytelni.
- Protokół wstępny czytelnika
- czytelnik czeka, jeśli w czytelni jest pisarz.
- Protokół wstępny pisarza
- pisarz czeka, jeśli w czytelni ktoś jest.
- Protokół końcowy czytelnika
- jeśli jest ostatnim wychodzącym, to wpuszcza pisarza (o ile czeka).
- Protokół końcowy pisarza
- jeśli czekają czytelnicy, to budzi ich wszystkich, w przeciwnym razie, jeśli czeka pisarz, budzi jednego.
Implementując taki algorytm dobrze jest wprowadzić, jak poprzednio, dodatkowy proces Czytelnia synchronizujący dostęp do czytelni i pełniący funkcję serwera. Proces ten przechowuje wszystkie informacje niezbędne do zaimplementowania powyższego algorytmu i decyduje, kogo można wpuścić do czytelni.
- Ćwiczenie
- Przeczytaj opis protokołów. Jakie zmienne będą potrzebne do ich zaimplementowania?
- Jak zrealizować zawieszenie czytelnika i pisarza?
- Czy czytelnicy i pisarze mogą oczekiwać w tym samym miejscu?
Schemat komunikacji
- Ćwiczenie (ukrywajka)
- Jakie komunikaty będą wymieniać procesy?
- Ile buforów jest potrzebnych?
Prowadzi to do definicji typów wyliczeniowych oraz zmiennych buforowych:
type Komunikat = (ChcęCzytać, ChcęPisać, Wychodzę); Zezwolenie = (Wejdź) var czytelnia, czytelnicy, pisarze: buffer;
oraz schematu komunikacji:
- Rysunek
Proces czytelnika i pisarza
Treść procesów jest prosta. Protokół wstępny polega na wysłaniu komunikatu do serwera i oczekiwaniu na zgodę, podobnie protokół końcowy to poinformowanie serwera, że pobyt w czytelni zakończył się:
process Czytelnik;
var
z: Zezwolenie;
begin
repeat
własne_sprawy;
SendMessage (czytelnia, ChcęCzytać);
GetMessage (czytelnicy, z);
CZYTANIE;
SendMessage (czytelnia, Wychodzę)
until false
end;
|
process Pisarz;
var
z: Zezwolenie;
begin
repeat
własne_sprawy;
SendMessage (czytelnia, ChcęPisać);
GetMessage (pisarze, z);
PISANIE;
SendMessage (czytelnia, Wychodzę)
until false
end;
|
Proces Czytelnia
Główna trudność to proces serwera. Musi on przechowywać liczniki:
- dc --- liczba działających czytelników, tj. czytelników przebywających w czytelni
- cc --- liczba czekających czytelników
- dp --- liczba działających pisarz, tj. pisarzy przebywających w czytelni
- cp --- liczba czekających pisarzy, tj.
- Ćwiczenie
- Które z powyższych zmiennych mogłyby być zmiennymi logicznymi zamiast licznikami?
Proces Czytelnia będziemy tworzyć stopniowo. Zacznijmy od deklaracji zmiennych lokalnych i początku głównej pętli:
process Czytelnia;
var
dc, dp : integer;
cc, cp : integer;
kom: Komunikat;
begin
dc := 0; dp := 0; cc := 0; cp := 0;
repeat
GetMessage (czytelnia, kom);
case kom of
ChcęCzytać:
...
Po odbiorze komunikatu serwer rozstrzyga do jakiego zdarzenia doszło (ChcęCzytać zgłosił czytelnik, który chce wejść do czytelni) i obsługuje je w odpowiedni sposób.
- Ćwiczenie (ukrywajka)
- Zaimplementuj obsługę komunikatu ChcęCzytać.
W podobny sposób implementujemy obsługę komunikatu ChcęPisać
- Ćwiczenie (ukrywajka)
- Zaimplemetuj ją.
Pozostała już tylko implementacja protokołu końcowego, który rozbija się na dwa fragmenty w zależności od wartości zmiennej dp:
Wyjdź:
if dp > 0 then { wychodzi pisarz }
begin
dec (dp);
if cc > 0 then { czekają czytelnicy }
while cc > 0 do { wpuszczamy wszystkich }
begin
SendMessage (czytelnicy, Wejdź)
dec (cc); inc (dc);
end
else if cp > 0 then { nie czekali czytelnicy, ale jest pisarz }
begin
inc (dp);
SendMessage (pisarze, Wejdź) { wpuszczamy jednego }
end
end else { wychodzi czytelnik }
begin
dec (dc);
if (dc = 0) and (cp > 0) then { jest to ostatni czytelnik i są pisarze }
begin
inc (dp); dec (cp);
SendMessage (pisarze, Wejdź)
end
end
end {case}
until false
end;