SOP121/ćw - temat G
Problemy współbieżności; język BACI.
Tam gdzie mamy współbieżnie działające procesy które ze sobą
współpracują
lub współzawodniczą o zasoby tam pojawiaja się problemy
współbieżności
...
Mały przykład problemu
współbieżności :
mamy zmienną "licznik" dostępną dla dwóch procesów P0 i P1;
początkowa wartość
licznika wynosi 0;
procesy P0 i P1 100-krotnie zwiększa licznik o 1 przy
pomocy następującego kodu:
licznik:= licznik + 1
{ co po przetłumaczeniu na assembler wygląda mniej więcej tak: }
l1: ax= licznik
l2: ax= ax + 1
l3: licznik= ax
w danej chwili wartość licznika powinna być dokładnie równa liczbie
wszystkich "zwiększeń o 1", a po zakończeniu działania obu procesów
powinno być: licznik = 200. Okazuje się że jeśli oba procesy
równocześnie zwiększają licznik to wynik końcowy może nie być
zadowalający - niebezpieczeństwo pojawia się wtedy gdy procesor
przełączy się na drugi proces między liniami l2 i l3. Aby uniknąć tego
niebezpieczeństwa
trzeba zapewnić aby w danej chwili tylko jeden proces zwiększał
licznik.
Innymi słowy trzeba potraktować operację zwiększania jako sekcje
krytyczną
i zapewnić wzajemne wykluczanie.
Definicje niektórych pojęć związanych ze
współbieżnością :
- zasoby krytyczne
- Tylko jeden proces może mieć w danej chwili dostęp do danego
zasobu krytycznego.
Takim zasobem może być np lista elementów znajdująca się w pamięci
operacyjnej,
na której proces chce dokonywac operacji dodawania/usuwania elementów.
Gdyby dwa procesy wykonywały takie operacje równocześnie to lista
prawdopodobnie
zostałaby uszkodzona (przy niekorzystnym zbiegu okoliczności).
- sekcja krytyczna
- Jest to fragment kodu, w którym dany proces używa zasobu
krytycznego. Tak
więc sekcja krytyczna jest zawsze związana z pewnym zasobem krytycznym
(zauważmy że dwa procesy mogą równocześnie przebywać w sekcjach
krytycznych różnych zasobów).
- wzajemne wykluczanie
- Mechanizm wzajemnego wykluczania musi zapewniać, że tylko jeden
proces
może przebywać w sekcji krytycznej związanej z danym zasobem. Zazwyczaj
na początku i na końcu sekcji krytycznej dodaje się pewien protokół
wstępny i protokół końcowy.
- blokada
- Inaczej zakleszczenie, deadlock. Wszystkie
procesy są wstrzymane
w oczekiwaniu na zdarzenie, które mogą spowodować właśnie te procesy.
- zagłodzenie
- Grupa procesów oczekuje na zdarzenie. Zdarzenie pojawia się
wiele ale "nasz"
proces nigdy nie jest wybierany. Mówimy o tym procesie, że został
zagłodzony.
- synchronizacja
procesów
lub wątków
- Jest to wstrzymywanie działania procesu
lub wątku
aby nie dopuścić do "utraty spójności danych"; np gdy dwa procesy chcą
wejść do sekcji krytycznej związanej z pewnym zasobem to jeden z nich
musi
być "wstrzymywany" tak długo aż drugi proces nie opuści sekcji
krytycznej.
Język programowania współbieżnego BACI.
Aby oswoić się z narzędziami używanymi do rozwiązywania "problemów
współbieżności"
będziemy rozwiązywać te problemy przy pomocy języka BACI
= (Ben-Ari Concurrent Interpreter).
Język ten występuje w 2 wersjach Pascalo-podobnej i C-podobnej ...
Wszystkie przykłady (pliki *.pm, *.cm i *.inc) jak i oprogramowanie
BACI
należy skopiować do własnego katalogu i tam wykonywać odpowiednie
eksperymenty
!
UWAGI od interpretera DOS-owego:
W sciezkach do katalogu nie powinno byc żadnych znaków zakazanych przez
DOS.
Sposób kompilowania i uruchamiania:
Uruchomienie kompilatora dla pliku zródłowego "np00.pm":
bapas np00
Uruchomienie interpretera:
bainterp np00
Kazdy program powinien wyswietlac jakies napisy, można go wtedy
zatrzymać
przy pomocy Ctrl-C.
Język BACI może służyc do eksperymentów z semaforami i monitorami,
ale
także do tworzenia nowych "narzędzi" i do eksperymentalnego ich
sprawdzania,
a to dzięki takim elementom jak: procedury niepodzielne ze słowem
kluczowym
atomic (które nie mogą być wywłaszczone), funkcje specjalne
takie
jak: suspend(), revive(), which_proc(). W szczególności można tworzyć
własne
semafory o zmodyfikowanej definicji.
Język BACI jest bardzo realistycznym symulatorem współbieżnych
procesów.
Jeśli zachodzi potrzeba przełączenia procesora, to nowy proces jest
wybierany
drogą losowania, tak więc nie można nic zakładać co do "względnej
prędkości"
działania procesów. Mówiąc dokładniej, jeśli mamy procesy ProcA i ProcB
wykonujące rozkazy A1,A2,A3, B1,B2,B3 (odpowiednio) to
rzeczywista
kolejność wykonania tych rozkazów jest dowolna, np: A1,A2,B1,A3,B2,B3
lub
A1,B1,B2,A2,A3,B3 (są to tzw "przeploty"; wszystkie przeploty są
możliwe).
W języku BACI posługujemy się pojęciem "proces" chociaż bardziej
odpowiednie
byłoby pojęcie "wątek", gdyż tym słowem posługuje się instrukcja
obsługi.
Język BACI (bapas.exe) jest bardzo podobny do uproszczonego języka
Pascal.
patrz: dokumentacja !!!
Uruchamianie wspólbieżnych procesów:
program np00;
atomic procedure pisz(nr, i:integer);
begin
writeln( nr, ":", i); i:=i+1;
end;
procedure proc(nr:integer);
var i:integer;
begin
i:=0;
while true do
begin
pisz(nr, i); i:=i+1;
end;
end;
begin
cobegin
proc(1);
proc(2);
proc(3);
coend;
end.
Powyższy kod powoduje uruchomienie 3 współbieżnie działających
procesów
z których każdy wykonuje pętle wyświetlającą napis nr_procesu:nr_iteracji.
Ogólne zasady działania programów w języku
BACI:
Każdy proces wykonuje kod pewnej procedury
(jednak kilka procesów może wykonywać kod tej samej procedury).
Każdy proces posiada własny stos, który jest używany do
implementacji zmiennych
lokalnych procedur
(dlatego każdy proces w powyższym przykładzie ma własną kopię zmiennej
"i").
Wszystkie procesy mają dostęp do zmiennych globalnych
(w powyższym przykładzie nie jest to wykorzystywane). |
UWAGA: Słowo atomic służy głównie do
definiowania
(niepodzielnych) "rozkazów procesora" o ile zakładamy że procesor jest
wyposażony w takie rozkazy; NIE należy go używać do rozwiązywania
problemów
chyba że wyraźnie się na to zezwala w treści zadania !.
Rozwiązując zadanie wolno używać TYLKO narzędzi które w danym
momencie
są omawiane !!!. |
Zadanie 90
Skompiluj i uruchom przykład "np00.pm". Spróbuj wziąć w komentarz
słowo "atomic" i zobacz co się zmieni w działaniu programu ...
Semafory.
Aby sprawdzić czy dane narzędzie jest skuteczne zazwyczaj rozwiązuje
się
przy jego pomocy następujące "klasyczne" problemy współbieżności:
Problem producenta i
konsumenta.
Producent produkuje elementy, a konsument je konsumuje jednak czynią to
z różnymi prędkościami. Używa się bufora do przechowywania elementów.
Gdy
bufor jest pusty konsument musi czekać, gdy bufor jest pełny producent
musi czekać. (To jest wersja problemu "ze skończonym buforem".)
Problem czytelników
i
pisarzy. Istnieje czytelnia i 2 grupy procesów: czytelnicy i
pisarze.
Chcą oni wchodzić do czytelni. W danej chwili w czytelni może przebywać
dowolna ilość czytelników lub tylko jeden pisarz. Ten problem to
"abstrakcja"
dostępu do bazy danych.
Problem pięciu
filozofów. Pięciu głodnych
filozofów siedzi przy okrągłym stole, przy talerzach z rybą. Aby jeść
rybę
potrzebne są 2 widelce, jednak między dwoma sąsiednimi filozofami leży
tylko 1 widelec. Wszyscy filozofowie na przemian myślą i jedzą (w
pętli).
Zauważ że wprowadzenie wzajemnego wykluczania dla każdego widelca nie
rozwiązuje
problemu: może się zdarzyć że wszyscy filozofowie zaczną podnosić
widelce
począwszy od lewego - wtedy nie beda mogli podnieść prawego widelca -
wystąpi
tzw blokada ... Druga uwaga: w problemie tym robi się zazwyczaj
zalożenie
że filozofowie NIE są ponumerowani "po kolei" w kolejności krzeseł przy
stole, gdyż w takim wypadku rozwiązanie jest łatwe (można wykorzystać
ten
numer).
Co należy rozumieć przez rozwiązanie problemu ?
Rozważmy to na przykładzie "problemu pięciu filozofów".
Mamy 5 procesów wykonujących funkcje Filozof():
int Jem[5]; // ta tablica zawiera informacje o tym
// który filozof je w danej chwili
void Filozof(int Nr) // Nr=0..4
{
while(1) {
// filozof mysli
Jem[Nr]=1;
// filozof je
Jem[Nr]=0;
}
}
void procesKontrolny()
{
while(1) {
cout <<Jem[0]<<Jem[1]<<Jem[2]<<Jem[3]<<Jem[4]<< endl;
}
}
Do procedury "Filozof" należy dodac fragmenty kodu zapewniające że
filozofowie
będą się zachowywać prawidłowo, zgodnie z definicją problemu pięciu
filozofów.
Np dwaj siedzący obok siebie filozofowie nie mogą równocześnie jeść (to
jest tzw własność bezpieczeństwa). Z drugiej strony każdy
filozof
powinien się kiedyś najeść (to jest tzw własność żywotności).
Bardzo
przydatny jest proces kontrolny, działający niezależnie od
innych,
który kontroluje bezpieczeństwo i żywotność (w tym wypadku po prostu
pokazuje
co robią filozofowie ...).
..................................................................
Jednym z narzędzi do rozwiązywania problemów współbieżności
(wynalezionym
przez Dijkstrę w 1965) jest semafor.
Definicja semafora ogólnego (wg Ben-Ariego):
Semafor to zmienna całkowita "S", na której można wykonywać następujące
(niepodzielne) operacje:
Opuszczenie, P(S), wait(S): Jeśli
S>0 to S=S-1,
w przeciwnym wypadku wstrzymaj działanie procesu wykonującego
tę
operację.
Podniesienie, V(S), signal(S): Jeśli są
procesy
wstrzymane w wyniku opuszczania semafora S, to wznów jeden z
nich,
w przeciwnym razie S=S+1.
Można nadać semaforowi wartość początkową.
Przykład: sekcja krytyczna chroniona przy pomocy semaforów ...
semaphore sem=1; // semafor ogólny, zainicjowany jedynką
void proc()
{
while(1) {
wait(sem); // protokół wstępny
//
// sekcja krytyczna
//
signal(sem); // protokół końcowy
//
// pozostałe czynności
//
}
}
main()
{
cobegin {
proc(); proc();
}
}
A oto kolejny prosty przykład (np02.pm).
Są tam dwie zmienne konto1 i konto2 między którymi
dokonuje
się "przelewów". Przelewów dokonuje wiele procesów naraz. Suma kont
musi
pozostać taka sama po dowolnym czasie. Jest jasne, że operacja
dokonywania
przelewu jest sekcją krytyczną, związaną z zasobem jakim są obie
zmienne.
Zadaniem procesu pokazKonta jest pokazywanie stanu zmiennych.
Wzajemne wykluczanie jest realizowane przy pomocy semaforów.
Zadanie 91
Przeanalizuj przykład "np02.pm". Poeksperymentuj trochę z tym
programem.
Spróbuj zmienić początkową wartość semafora na 0 oraz na 2. Spróbuj
także
wziąć w komentarz wywołania wait() i signal().
Zadanie 91a
Zaprojektuj eksperyment w języku BACI pokazujący niebezpieczeństwo
o którym mowa tutaj. Powinny istnieć
zmienne
globalne "licznik", "i1", "i2" oraz 2 procesy "proc1" i "proc2". Proc_k
(k=1,2) zwiększa o 1 zmienna "licznik" oraz "i_k". Istnieje także
proces kontrolny procKontr pokazujący dwie wartości: i1+i2 oraz
licznik. Oczywiści
gdyby wszystko działało prawidłowo to te dwie liczby powinny być równe
!. Kod zwiększający zmienne traktujemy jako sekcje krytyczną. Wykonaj
eksperyment
z ochroną sekcji krytycznej (przy pomocy semafora) i bez ...
Do sprawozdania wstaw kody programów w języku BACI oraz końcówki
produkowanych przez nie wydruków.
Zadanie 91b
Zbadaj skąd się bierze błędne działanie przykładu "np02.pm"
(niewłaściwa suma konto1+konto2) jeśli
usuniemy operacje semaforowe. Czy problem powstaje gdy procesor
przełącza
się na inny proces między modyfikacją zmiennej konto1 a modyfikacją
zmiennej konto2 czy też są jakieś inne przyczyny błędu?
Definicja semafora binarnego:
Semafor binarny to zmienna "S", przyjmująca wartości 0 i 1, na której
można wykonywać następujące (niepodzielne) operacje:
Opuszczenie, PB(S), wait(S): Jeśli
S==1 to
S=0, w przeciwnym wypadku wstrzymaj działanie procesu
wykonującego
tę operację.
Podniesienie, VB(S), signal(S): Jeśli są
procesy
wstrzymane w wyniku opuszczania semafora S, to wznów jeden z
nich,
w przeciwnym razie S=1.
Można nadać semaforowi wartość początkową, 0 lub 1.
Uwaga !!!:
w języku BACI jeśli S=1 to signal(S) powoduje błąd! (jest to odstępstwo
od tradycyjnej definicji semafora binarnego mające na celu ułatwienie
debugowania)
Zasada programowania z użyciem semaforów:
1. Interpretacja wartości semafora powinna być tak dobrana, aby
operacje
wait() i signal() były dla nas przydatne i
wygodne.
2. Interpretacja ta powinna być zapisana w komentarzu obok deklaracji
semafora. |
Poniżej znajdują się zadania dotyczące semaforów.
Nieco trudniejsze zadania będą oznaczone przez (*) i (**).
Zadanie 92
Oto niepoprawne rozwiązanie problemu "producenta/konsumenta",
dla nieskończonego bufora, przy użyciu semaforów binarnych oraz
zmiennej
pomocniczej "N" (np03.cm). W przykładzie tym
produkowane
elementy są wybierane losowo, konsument wyświetla co skonsumowal a
producent
co wyprodukował ...
Należy przeanalizować i zrozumieć na czym to rozwiązanie polega. Potem
proszę znaleźć błąd i naprawić go. Aby ułatwić sobie analizę działania
programu, uruchamiaj interpreter z przekierowaniem do pliku (bainterp
np03
> qqq.txt), oczywiście jeśli jego działanie jest "skończone" np z
powodu
błędu. Dokładnie przeanalizuj działanie programu tuż przed wystąpieniem
błędu. Zobacz co się dzieje z semaforem "Zwłoka". Linie "atrapa=321"
mają
na celu zwiększenie prawdopodobieństwa wystąpienia błędu.
Uwaga: kod trzeba przetłumaczyć na język BACI/Pascal !.
Wskazówka: coś niedobrego zaczyna się dziać gdy:
1) N=0, Zwloka=0, konsument wychodzi ze swojej sekcji krytycznej
2) producent wykonuje całą swoją sekcje krytyczną (zmieniając wartość
m.in. N !)
3) N=1, Zwloka=1, konsument wykonuje "if( N==0 ) ..."
void Producent() { int atrapa;
while(1) { wait(S); // poczatek sekcji kryt. Produkuj(); N++; if( N==1 ) signal(Zwloka); signal(S); // koniec sekcji kryt.
atrapa=321; atrapa=321; atrapa=321; } }
|
void Konsument() { int atrapa; string[20] str;
wait(Zwloka); while(1) { wait(S); // poczatek sekcji kryt. Konsumuj(); N--; signal(S); // koniec sekcji kryt.
atrapa=123; atrapa=321; atrapa=123;
if( N==0 ) wait(Zwloka); } }
|
Zadanie 93
Rozwiąż problem "producenta/konsumenta",
dla
skończonego bufora. Zakładamy, że jest 1 producent i 1 konsument. Użyj
m.in. 2 semaforów ogólnych Pełne i Wolne. "Pelne" będzie interpretowany
jako ilość elementów bufora które można skonsumować, "Wolne" będzie
interpretowany
jako ilość wolnych elementow w buforze. Jest to najbardziej naturalne
zastosowanie
semaforów ogólnych. Można wykorzystać część kodu z pliku "np03.cm". Aby
skontrolować działanie programu należy "produkować" kolejne liczby
calkowite,
konsument powinien drukowac na ekranie to co "konsumuje", w ten sposób
dowiemy się czy wszystko sprawnie działa ...
Zadanie 93a
Rozwiąż problem "producenta/konsumenta",
dla
skończonego bufora. Tym razem zakładamy, że jest kilku producentów i
kilku
konsumentów. Dodatkowo odpowiedz na pytania:
Czy kilku (>1) konsumentów może równocześnie wykonywać swoje
operacje na
buforze ?.
Czy 1 konsument i 1 producent mogą równocześnie wykonywać swoje
operacje
na buforze ?.
Aby skontrolować działanie programu niech każdy producent produkuje
kolejne liczby z innego zakresu (np Prod0 od 0, Prod1 od 1000, Prod2 od
2000) a konsumenci powinni wypisywac to co konsumują.
Zadanie 93b (*)
Rozwiąż problem "producenta/konsumenta",
dla
skończonego bufora, przy użyciu semaforów binarnych. Zakładamy, że jest
1 producent i 1 konsument. Można wykorzystać część kodu oraz pomysł z
pliku
"np03.cm".
Zadanie 94
To jest przykładowe rozwiązanie (np05.cm+np05.inc)
problemu "czytelników i pisarzy",
w przypadku gdy jest znany rozmiar czytelni. Zwróć uwagę na to,
że czytelnia jest "zaimplementowana" w wyraźny sposób (zmienne Ilość*;
procedury wchodzenia i wychodzenia z czytelni). Poprawność rozwiązania
jest sprawdzana przez proces _Kontrola*, który bez przerwy
sprawdza
stan czytelni. Nie sprawdza się jednak zagłodzenia procesów.
Niewykluczone
że wpuszcza się tylko pisarza nr 1, a nie wpuszcza nigdy pisarza nr 2.
Dodaj kontrolę ewentualnego zagłodzenia do procesu kontrolnego
(powinien
on pokazywać które konkretnie procesy są w czytelni). Ponadto dodaj na
końcu statystykę demonstrującą ile razy który proces wszedł do
czytelni.
Tak więc proces kontrolny powinien wyświetlać napisy:
1 0 0 | 0 0 0 0 0 0 0 0 0 0
0 0 0 | 1 0 1 0 0 0 0 0 0 0
0 0 0 | 1 0 1 0 1 1 1 1 0 0
0 1 0 | 0 0 0 0 0 0 0 0 0 0
............................
# jest 3 pisarzy i 10 czytelników; pokazujemy kto w danej chwili
jest w czytelni
# na koncu dodatkowo "statystyka" ile razy dana osoba była w czytelni
10 10 9 | 11 10 10 10 12 9 8 10 11 12
Dodatkowo odpowiedz na pytanie: co by się stało gdyby nie było
wzajemnego
wykluczania pisarzy.
Zadanie 95
Rozwiąż problem "pięciu filozofów"
używając - m.in.- po jednym semaforze binarnym na każdy "widelec"
(semafory
te mają być przechowywane w tablicy). Każdy filozof na przemian myśli
lub
usiłuje zjeść kawałek ryby, do czego potrzebuje 2 widelców. Zauważ, że
jeśli wszyscy filozofowie podniosą lewy widelec to wystąpi blokada.
Przedstaw
ogólne rozwiązanie dla N-filozofów i uzasadnij jego poprawność. Koniecznie
dodaj proces kontrolny sprawdzający co w danej chwili robią wszyscy
filozofowie
(jedzą czy myślą); proces ten powinien wyświetlać następujące napisy:
001000001000100
000011001000100
000000000100101
.............................
# to jest eksperyment z 15-ma filozofami ...
# 1 oznacza ze filozof "je", 0 oznacza ze filozof "myśli"
# jak widać w rozwiązaniu jest błąd bo 3 i 4 filozof w pewnym momencie
równocześnie jedzą !
Wskazówka: Można uniknąć blokady ograniczając liczbę filozofów
którzy
chcą równocześnie jeść.
Uwaga: "Nr" filozofa może być używany wyłącznie do
uzyskiwania
dostępu do odpowiedniego widelca!
Zadanie 96 (**)
Jest to inna wersja rozwiązania problemu "pięciu
filozofów". W tej wersji obliczenia są bardziej "równoległe" tj
większa
ilość filozofów je równocześnie, w porównaniu z rozwiązaniem
sugerowanych
w zadaniu poprzednim. Każdy filozof próbując przystąpić do jedzenia
sprawdza, czy
któryś z jego sąsiadów je. Jeśli tak jest, to nasz filozof zostaje
"uśpiony"
(oczywiście przy pomocy semafora). Gdy pewien filozof skończy jeść, to
próbuje obudzić swoich sąsiadów, którzy mogli zasnąć próbując jeść.
Filozof nr "i" powinien mieć przyporządkowane:
zmienną "state[i]", przyjmującą
wartości "THINKING", "HUNGRY", "EATING"
semafor "s[i]", zainicjowany
zerem.
Operacje na zmiennej "state[i]" oraz sprawdzanie stanu sąsiadów
powinny
być chronione semaforem binarnym "mutex".
[przed jedzeniem]
Gdy filozof nr "i" próbuje jeść to zaczyna od przypisania
"state[i]=HUNGRY",
następnie sprawdza sąsiadów: jeśli żaden z nich nie je to zmienia swój
stan "state[i]=EATING" oraz wykonuje "signal(s[i])"; następnie wykonuje
"wait(s[i])" (jeśli "signal(s[i])" nie zostało wykonane to "wait(s[i])"
uśpi naszego filozofa).
[po jedzeniu]
Gdy filozof nr "i" kończy jedzenie to zmienia swój stan
"state[i]=THINKING",
a następnie koncentruje swą uwagę na sąsiadach: jeśli któryś z nich
został
uśpiony w czasie próby jedzenia (tj jego state[.]==HUNGRY) i może jeść,
to zmienia jego stan na EATING oraz budzi go przy pomocy "signal(s[.])".
Przydatna będzie następująca funkcja:
void test(i)
{
// .....................
if( (state[i]==HUNGRY)
&& (state[LEFT]!=EATING)
&& (state[RIGHT]!=EATING) )
{
state[i]=EATING; signal(s[i]);
}
}
Przedstaw ogólne rozwiązanie dla N-filozofów. Proces kontrolny
powinien
pokazywać co w bieżącej chwili robią wszyscy filozofowie (jedzą czy
myślą). Porównaj to rozwiązanie z rozwiązaniem poprzedniego
zadania pod kątem
równoległości obliczeń (należy porównać wydruki generowane przez oba
rozwiązania).
Aby zobaczyć efekt zwiększonej "równoległości jedzenia" spowoduj aby
jedzenie
trwało długo (100 x atrapa=123), a myślenie krótko (2 x atrapa=123).
Monitory
Teraz omówimy następne "narzędzie" do rozwiązywania problemów
współbieżności
- tzw monitor - podobno jest on lepszy od semafora gdyż jest
mniejsze
prawdopodobieństwo błędnego programowania.
Dygresja: W jaki sposób s.o. udostępnia monitory
skoro wydaje się że monitor stanowi część języka programowania ? Odp:
s.o.
udostępnia pewne elementarne funkcje, które pozwalają skonstruować
monitor
wykorzystując jako podstawowe tworzywo np klasy C++ ... (patrz:
Linux/Unix
i biblioteka Pthread).
Oto przykład monitora o nazwie "Konta":
monitor Konta {
int konto1=12345, konto2=0;
void DokonajPrzelewu(int przelew)
{
konto1=konto1-przelew;
konto2=konto2+przelew;
}
}
Definicja
Monitora.
Monitor jest zbiorem procedur i zmiennych, spełniającym następujące
warunki:
-
Zmienne są widoczne tylko wewnątrz monitora (tj wewnątrz bloku
monitora),
natomiast procedury są widoczne na zewnątrz. Tak więc dostęp do
zmiennych
monitora jest możliwy tylko poprzez procedury monitora.
-
W danej chwili tylko jeden proces może przebywać w monitorze
(tj
wykonywać jego procedurę), co umożliwia prostą realizację wzajemnego
wykluczania.
-
Istnieją dwa rodzaje kolejek zawierających wstrzymane procesy: kolejka
procesów zewnętrznych (próbujących uruchomić procedurę
monitora)
oraz kolejki związane ze zmiennymi typu conditon.
-
Gdy monitor jest zajęty (pewien proces wykonuje jedną z jego procedur)
i nastąpi próba wywołania procedury monitora przez inny proces, to ten
inny proces zostanie wstrzymany i umieszczony na końcu kolejki
procesów
zewnętrznych. Gdy monitor zostanie zwolniony, to wtedy wznawiany jest
pierwszy
proces z kolejki procesów zewnętrznych.
-
Istnieje możliwość wstrzymywania i wznawiania procesów
wewnątrz
monitora przy pomocy zmiennych typu condition, na których można
wykonywać operacje "waitc()" oraz "signalc()".
-
Po wykonaniu waitc(c,p) gdzie "c" jest
zmienna
typu conditon a "p" jest priorytetem (liczbą całkowitą) proces
jest
usypiany i umieszczany w kolejce związanej ze zmienną "c" (priorytet
też
jest zapamiętywany). Monitor jest zwalniany, może go zając inny proces.
Wykonanie "waitc(c)" to to samo co "waitc(c,10)".
-
Po wykonaniu signalc(c) budzony jest
jeden
z procesów z najwyższym priorytetem (najmniejsza liczba), wybrany
losowo,
w kolejce zmiennej "c", a proces który wykonał "signalc()" jest
usypiany
i umieszczany na początku kolejki procesów zewnętrznych.
-
Istnieje możliwość sprawdzenia, czy kolejka zmiennej "c" jest pusta
przy
pomocy "empty(c)".
To jest przykład użycia monitora do wzajemnego wykluczania (np06.cm+np02.inc).
To jest przykład implementacji semafora ogólnego przy pomocy monitora
(np07.cm+np02.inc).
UWAGA 1: powyższa def
monitora
pochodzi z książki Weiss/Gruźlewski; wydaje się że monitory języka BACI
są nieco inne: procesy z "kolejki procesów zewnętrznych" są wybierane
losowo
tak że nie ma znaczenia czy proces jest umieszczana na końcu czy
początku
tej kolejki ...
UWAGA 2: jeśli proces kontrolny ma pokazywać wartości
rozmaitych
zmiennych (np ilosc czytelników czy pisarzy w czytelni) to te zmienne
należy
umieścić poza monitorem, przynajmniej w czasie testowanie programu.
Zadanie 99
Zaimplementuj przy pomocy monitora semafor, w którym wstrzymane
procesy
nie są wybierane losowo lecz przy pomocy kolejki FIFO (wykorzystaj
priorytety
!). Dodatkowo wykonaj eksperyment pokazujacy ze to jest faktycznie
semafor
FIFO - pokaz ze procesy sa wznawiane w tej samej kolejnosci
w jakiej byly wstrzymywane (przyda sie procedura which_proc zwracajaca
nr procesu
ktory ja wywolal).
Zadanie 100
Rozwiąż problem "producenta/konsumenta"
przy pomocy monitora. (Skończony) bufor powinien znajdować się w
monitorze.
Powinny być dwie zmienne condition, służące do wstrzymywania,
odpowiednio,
producentów i konsumentów.
Zadanie 101 (*)
Podaj rozwiązanie problemu "czytelników
i pisarzy", bez założenia, że rozmiar czytelni jest znany, używając
monitora. W monitorze powinny być 2 zmienne typu "int": IloscPisarzy i
IloscCzytelnikow, reprezentujące ilość osób przebywających w czytelni,
oraz 2 zmienne typu "condition": Pisarze, Czytelnicy, służące do
wstrzymywania
osób przed wejściem do czytelni. Dla obu kategorii osób potrzebne będą
procedury monitora do wchodzenia i wychodzenia z czytelni.
Uwaga: w tym zadaniu procedury Czytelnik/Pisarz-Wchodzi/Wychodzi
maja inne znaczenie niz w zadaniu 94, przyklad np05.cm; w obecnym
zadaniu procedury te sluza jedynie jako sekcja wejsciowa i wyjsciowa
uruchamiane przed wejsciem i po wyjsciu osoby z czytelni ...
Wskazówka: postaraj się zrealizować następujący scenariusz:
CzytelnikWchodzi:
jeśli nie ma pisarza pracującego lub oczekującego to wejdź, w
przeciwnym
razie czekaj |
PisarzWchodzi:
jeśli w czytelni są czytelnicy lub pisarz to czekaj, w przeciwnym
wypadku
wejdź |
CzytelnikWYchodzi:
jeśli jestem ostatnim czytelnikiem opuszczającym czytelnie to wpuść
pisarza |
PisarzWYchodzi:
jeśli nie ma oczekujących czytelników, to wpuść następnego pisarza,
w przeciwnym przypadku wpuść wszystkich czytelników
(w czasie wpuszczania czytelników sprawdzaj
czy
do czytelni nie wszedł pisarz ...) |
Co się może stać jeśli nie uwzględnimy tekstu zapisanego na czerwono
? (opisz dokładnie przeplot zdarzeń prowadzący do błędu).
Mechanizmy niskiego poziomu.
Będziemy się teraz zajmować konstruowaniem "narzędzi niskiego
poziomu"
do wzajemnego wykluczania (bardziej elementarnych niż semafory).
Zadanie 102
(algorytm Petersena)
Zaprogramuj narzędzie do wzajemnego wykluczania dla 2 procesów,
przy
pomocy samej tylko "dzielonej" pamięci (w przypadku j. BACI - zmienne
globalne).
W tym celu użyj 2 zmiennych:
int flag[2]; // elementy przyjmują wartości 0 i 1
int turn; // przyjmuje wartości 0 i 1
Mamy 2 procesy "P0" i "P1".
Gdy proces "Pi" próbuje wejść do sekcji krytycznej to przypisuje
flag[i]=1,
a następnie czeka aż przeciwnik opuści sekcję krytyczną (pętla while -
jest to tzw aktywne czekanie). Gdy to nastąpi, "Pi" wchodzi do
sekcji
krytycznej.
Gdy proces "Pi" wychodzi z sekcji krytycznej to przypisuje flag[i]=0.
Zauważ, że z tego co do tej pory zostało powiedziane wynika, że gdyby
oba procesy próbowały równocześnie wejść do sekcji krytycznej, to przy
nieszczęśliwym zbiegu okoliczności, oba mogłyby zostać zablokowane - i
tutaj pojawia się potrzeba użycia zmiennej "turn", proces "Pi"
przypisuje
jej wartość "i" przed sekcją krytyczną. Procesy P0 i P1 przypisują tej
zmiennej 0 i 1, ale oczywiście zmienna może mieć tylko jedną wartość -
należy to odpowiednio wykorzystać w aktywnym czekaniu - gdyby oba
procesy
próbowały równocześnie wejść do sekcji krytycznej, to tylko jeden z
nich
zostanie zablokowany.
Zaimplementuj ten algorytm w języku BACI i sprawdź czy
rzeczywiście
mamy wzajemne wykluczanie i czy żaden z procesów nie jest
zagłodzony.
Przygotuj specjalne funkcje do uruchamiania przy wchodzeniu i
wychodzeniu
ze sekcji krytycznej:
// proces P0
Prolog0();
// sek kryt
Epilog0();
// proces P1
Prolog1();
// sek kryt
Epilog1();
UWAGA: kolejność modyfikowania zmiennych turn i flag[] przed sek
kryt ma duże znaczenie !. Sprawdź eksperymentalnie która kolejność jest
prawidłowa (i dołącz wydruki do sprawozdania).