Programowanie współbieżne i rozproszone/PWR Ćwiczenia 3: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
(Nie pokazano 4 pośrednich wersji utworzonych przez tego samego użytkownika) | |||
Linia 42: | Linia 42: | ||
: O czym powinien informować serwer? | : O czym powinien informować serwer? | ||
: Jakie bufory są potrzebne? | : Jakie bufory są potrzebne? | ||
<div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | <div class="mw-collapsible mw-made=collapsible mw-collapsed"> Odpowiedź | ||
<div class="mw-collapsible-content" style="display:none">Każdy proces powinien wysyłać serwerowi informację o tym, że chce wejść do sekcji krytycznej. Serwer, po odebraniu takiego komunikatu, sprawdza, czy sekcja krytyczna jest wolna. Jeśli tak, to wysyła procesowi zgodę na wejście do niej. Proces powinien cierpliwie oczekiwać na zgodę serwera, po czym wejść do sekcji krytycznej dopiero po jej otrzymaniu. Następnie po zakończonym pobycie w sekcji musi poinformować serwer, że jest ona wolna. | <div class="mw-collapsible-content" style="display:none">Każdy proces powinien wysyłać serwerowi informację o tym, że chce wejść do sekcji krytycznej. Serwer, po odebraniu takiego komunikatu, sprawdza, czy sekcja krytyczna jest wolna. Jeśli tak, to wysyła procesowi zgodę na wejście do niej. Proces powinien cierpliwie oczekiwać na zgodę serwera, po czym wejść do sekcji krytycznej dopiero po jej otrzymaniu. Następnie po zakończonym pobycie w sekcji musi poinformować serwer, że jest ona wolna. | ||
Opisany schemat daje się zrealizować za pomocą dwóch buforów: | Opisany schemat daje się zrealizować za pomocą dwóch buforów: | ||
* w pierwszym <tt>zam</tt> procesy umieszczają "zamówienia" na sekcję krytyczną | * w pierwszym <tt>zam</tt> procesy umieszczają "zamówienia" na sekcję krytyczną | ||
Linia 57: | Linia 55: | ||
: Napisz treść procesu | : Napisz treść procesu | ||
<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ź | ||
Przypuśćmy, że opisane powyżej komunikaty definiujemy za pomocą wartości typu wyliczeniowego: | <div class="mw-collapsible-content" style="display:none">Przypuśćmy, że opisane powyżej komunikaty definiujemy za pomocą wartości typu wyliczeniowego: | ||
'''type''' | '''type''' | ||
Komunikat = (Chcę, Możesz, Zwalniam); | Komunikat = (Chcę, Możesz, Zwalniam); | ||
Linia 86: | Linia 84: | ||
: Czy wprowadzenie numerów procesów jest rzeczywiście niezbędne? | : Czy wprowadzenie numerów procesów jest rzeczywiście niezbędne? | ||
<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ź | ||
Oprócz liczby wolnych miejsc w sekcji krytycznej serwer zapamiętuje jeszcze, ile procesów poprosiło o zgodę na wejście do sekcji i jej nie dostało. Gdy zwolni się miejsce w sekcji i są chętni do wejścia do sekcji, to serwer umieszcza jedno zezwolenie w buforze <tt>zezw</tt>. | <div class="mw-collapsible-content" style="display:none"> Oprócz liczby wolnych miejsc w sekcji krytycznej serwer zapamiętuje jeszcze, ile procesów poprosiło o zgodę na wejście do sekcji i jej nie dostało. Gdy zwolni się miejsce w sekcji i są chętni do wejścia do sekcji, to serwer umieszcza jedno zezwolenie w buforze <tt>zezw</tt>. | ||
</div></div> | </div></div> | ||
Linia 93: | Linia 91: | ||
: Napisz kod serwera | : Napisz kod serwera | ||
<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ź | ||
A oto przykładowy kod serwera: | <div class="mw-collapsible-content" style="display:none">A oto przykładowy kod serwera: | ||
'''process''' Dozorca; | '''process''' Dozorca; | ||
'''var''' | '''var''' | ||
Linia 181: | 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 243: | 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 315: | 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 337: | 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;