SOP322/ćw - temat A
Problemy współbieżności.
Tam gdzie mamy współbieżnie działające procesy które ze sobą
współpracują lub współzawodniczą o zasoby tam pojawiają 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ą (patrz Weiss/Gruźlewski)
:
- proces
- Proces to uruchomiony, działający program. Każdy proces ma
swoją własną przestrzeń adresową dlatego dwa procesy nie mogą (w
naturalny sposób) używać tych samych zmiennych. Istnieją jednak
narzędzia do komunikacji międzyprocesowej oraz do synchronizowania
procesów.
- wątek
- W pojedynczym procesie może działać kilka wątków. Każdy wątek
wykonuje kod programu, przy czym różne wątki mogą być w różnych
"miejscach" kodu. Wątki działają w jednej przestrzeni adresowej
(należącej do procesu), dlatego mają wszystkie dostęp do zmiennych
globalnych. Każdy wątek ma własny stos, dlatego każdy wątek ma swoje
własne zmienne lokalne.
- 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.
- przełączanie procesora, przełączenie
kontekstu, wywłaszczenie procesu
- Gdy mamy kilka procesów w maszynie z jednym procesorem, to
procesor ten musi obsługiwać "po trochu" wszystkie procesy. Operację w
której procesor przechodzi od jednego procesu do drugiego nazywamy przełączeniem
procesora lub przełączeniem kontekstu. Trzeba wtedy
zapamiętać stan (m.in. wartości rejestrów procesora) jednego procesu i
odtworzyć stan drugiego. O procesie który "stracił" procesor mówimy, że
został wywłaszczony.
- rodzaje interakcji między procesami
- 1. Procesy nieświadome swojego istnienia. Pojawia się problem
współzawodnictwa w dostępie do zasobów krytycznych (niezbędna jest
synchronizacja procesów).
- 2. Procesy współpracujące przez "dzielenie". Może to być
dzielenie pliku lub pamięci operacyjnej.
- 3. Procesy współpracujące przez wysyłanie komunikatów.
- 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 ... (my będziemy używać wersji
C-podobnej).
Inne materiały na temat BACI (w tym kompilator dla linuxa): http://www.mines.edu/fs_home/tcamp/baci/
Wszystkie przykłady (pliki *.cm i *.inc) jak i oprogramowanie BACI
należy skopiować do własnego katalogu i tam wykonywać odpowiednie
eksperymenty !
UWAGA od interpretera DOS-owego: w ścieżki do plików muszą
być zgodne z normami DOS (nawet jeśli pracujemy pod winXP).
Sposób kompilowania i uruchamiania.
Uruchomienie kompilatora dla pliku źródłowego "np01.cm" :
bacc np01
powoduje utworzenie pliku z PCODE-m. Uruchomienie interpretera PCODE-u
:
bainterp np01
każdy program powinien wyświetlać jakieś napisy; można go wtedy
zatrzymać przy pomocy Ctrl-C.
Ogólne zasady działania programów w języku BACI.
Przykład programu w języku BACI (wersja C-podobna) :
int licznik=123;
void proc(int nr)
{
int i=0;
while(1) { cout<<nr<<":"<<i<<endl; i++; }
}
main()
{
cobegin {
proc(1); proc(2); proc(3);
}
}
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"
(niestety, ten przykład nie działa tak jak się tego spodziewamy gdyż
napisy się mieszają ...).
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.
Każdy proces wykonuje kod pewnej procedury (jednak kilka procesów
może wykonywać kod tej samej procedury, jak w powyższym przykładzie).
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.
Język BACI może służyć do eksperymentów z tzw semaforami i monitorami,
ale także do tworzenia nowych "narzędzi" i do eksperymentalnego ich
sprawdzania, a to dzięki takim elementom jak:
- funkcje niepodzielne ze słowem kluczowym atomic; proces który
wykonuje taką funkcję nie może zostać wywłaszczony;
- funkcje specjalne suspend(), revive(); służą do usypiania/ budzenia
procesu.
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 i B1,B2,B3
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 !).
Język BACI jest bardzo podobny do (uproszczonego) języka C.
Spośród typów danych j. C wolno używać "int" i "char".
Dodatkowo wprowadzono typ "string" i funkcje typu
"stringCopy(dest,src)".
Wolno używać tablic.
Parametry funkcji są przekazywane przez wartość chyba że użyjemy znaku
"ampersand".
UWAGA: BACI ma liczbe ograniczenia syntaktyczne w porównaniu z
j. C
Nie należy używać złożonych konstrukcji języka C !!!
np zamiast
tab[i++]=j;
należy napisać
tab[i]=j; i++;
Bardziej złożony przykład programu w BACI (np01.cm).
W przykładzie tym mamy 3 współbieżne procesy, nazwane AAA, BBB, CCC;
każdy proces posiada lokalną zmienną "i"; każdy procesy wykonuje pętle,
w której jest wyświetlana nazwa procesu oraz wartość zmiennej "i"
(zwiększana o 1). Jednak w tym wypadku napisy się nie mieszają dzięki
zastosowaniu proc2() i słowa atomic; w procedurze zdefiniowanej
ze słowem atomic nie może nastąpić przełączenie procesora na inny
proces.
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 !!!. |
Semafory.
Będziemy się teraz zajmować pewnymi narzędziami służącymi do
rozwiązywania problemów współbieżności ...
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
// czyli zawiera "stan" ...
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ą.
Semafor ogólny może być w oczywisty sposób używany do realizacji
wzajemnego wykluczania, przy czym wait(S) pełni funkcję
protokołu wstępnego, a signal(S) protokołu końcowego. Zauważmy,
że początkowa wartość semafora to największa ilość procesów, które mogą
równocześnie przebywać w sekcji krytycznej. Semafor może być jednak
używany także do całkiem innych celów, co zaobserwujemy wykonując
zadania.
Zauważmy, że nie określamy w jaki sposób jest wybierany proces,
który ma zostać wznowiony podczas podnoszenia semafora. W języku BACI
proces jest wybierany drogą losowania. Można jednak zaimplementować
własny semafor, w którym oczekujące procesy będą wybierane w kolejności
zgłoszeń, tj będą się znajdowały na liście FIFO.
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();
}
}
Prosty przykład (np02.cm+ np02.inc).
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 3
Zaprojektuj eksperyment w języku BACI pokazujący niebezpieczeństwo
o którym mowa tutaj.
Wskazówki: powinny istnieć zmienne globalne "licznik", "i1",
"i2" oraz dwa procesy "proc1" i "proc2". Każdy z procesów wykonuje
nieskończoną pętle, w której zwiększa o 1 zmienną licznik oraz "swoją"
zmienna i_k (gdzie k to nr procesu). Musi istnieć proces kontrolny
"procKontr" wyświetlający dwie wartości: i1+i2 oraz licznik (w
nieskończonej pętli). Oczywiście gdyby wszystko działało prawidłowo to
te dwie liczby powinny być równe !. Do sprawozdania wstaw kod programu
w języku BACI oraz końcówkę wydruku.
Następnie kod zwiększający zmienne licznik oraz i_k potraktuj
jako "sekcję krytyczną" i chroń ją przy pomocy semafora (zapewnij
wzajemne wykluczanie). Teraz wszystko powinno działać prawidłowo. Do
sprawozdania wstaw kod programu w języku BACI oraz końcówkę wydruku.
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.
Semafor binarny nie jest szczególnym przypadkiem semafora
ogólnego, gdyż "nie pamięta" ilości operacji podnoszenia.
UWAGA: Niekiedy podniesienie
podniesionego semafora binarnego jest traktowane jako błąd (tak jest
m.in. w przypadku języka BACI).
Wskazówki
co do 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 ...
Zadanie 5
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 (liczby) są wybierane losowo, konsument
wyświetla co skonsumował a producent co wyprodukował ...
Należy przeanalizować i zrozumieć na czym to rozwiązanie polega. Potem
proszę znaleźć błąd, opisać w sprawozdaniu na czym polega i naprawić
go.
Tak więc w sprawozdaniu należy umieścić:
- opis błędu + wydruk pokazujący błąd
- poprawiony kod procedur Producent() i Konsument()
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.
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 6
Rozwiąż problem "producenta/konsumenta".
Zakładamy że:
1. bufora jest skończony
2. jest jeden producent i kilku konsumentów
Wskazówka: użyj 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 elementów
w buforze. Dodatkowo nie wolno dopuścić do sytuacji w której 2
konsumentów konsumuje ten sam element itp. Aby skontrolować działanie
programu niech producent produkuje kolejne liczby całkowite, a
konsumenci powinni wypisywać na ekranie to co konsumują. Jeśli
wszystko działa poprawnie to powinniśmy zobaczyć na ekranie ciąg
kolejnych liczb. W sprawozdaniu umieść tylko istotne procedury i
wydruk działania.
Zadanie 8
Rozwiąż problem "producenta/konsumenta",
dla skończonego bufora, używając WYŁĄCZNIE semaforów binarnych.
Zakładamy, że jest 1 producent i 1 konsument. Można wykorzystać część
kodu oraz pomysł z pliku "np03.cm".
Zadanie 9
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 poprzez zmienne Ilość*.
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 wyjaśnij działanie algorytmu przedstawionego w
przykładzie np05.cm (wyjaśnij rolę semaforów W i R).
Zadanie 10
Rozwiąż problem "pięciu filozofów".
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ą !
Uwaga: "nr filozofa" może być używany wyłącznie do
uzyskiwania dostępu do odpowiedniego widelca.
Zadanie 11
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 (N<=15). Proces
kontrolny
powinien pokazywać co w danej 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" trzeba odpowiedno podopisywać "atrapy"
spowolniające działanie kodu (i zwiekszające prawdopodobieństwo
przebywania procesora w pewnych obszarze kodu); zrób to tak żeby
istotnie zobaczyć przewagę rozwiązania z tego zadania ...
Zadanie 12
Podaj rozwiązanie problemu "czytelników i pisarzy", gdy rozmiar
czytelni nie jest znany.
Wskazówka: Są 2 grupy procesów; czytelnicy i pisarze. Każdy
proces pragnący wejść do czytelni sprawdza czy są (oczekujące na
wejście) procesy przeciwnej grupy; jeśli ich nie ma to wpuszcza
wszystkie oczekujące przed nim procesy swojej grupy. Następnie sam
próbuje wejść do czytelni. Po wyjściu z czytelni proces, który
stwierdził że jest ostatni wpuszcza oczekujące procesy przeciwnej
grupy. Oczywiście pisarze muszą wchodzić do czytelni pojedynczo.
Należy (koniecznie) użyć zmiennych: OczekCzyt, PracCzyt,
OczekPisarze, PracPisarze, które zliczają osoby oczekujące na
wejście
i pracujące w czytelni; potrzebne będą także 2 semafory ogólne służące
do wstrzymywania czytelników i pisarzy: Czyt, Pisarze; a także
semafor binarny do wzajemnego wykluczania pisarzy oraz semafor binarny
do ochrony zmiennych Oczek* i Prac*.
Co to znaczy "wpuścić pisarza" ? Odp: oznacza to wykonanie operacji signal(Pisarze)
przy założeniu, że każdy pisarz zanim wejdzie do czytelni wykonuje wait(Pisarze).
Oto NIEpoprawne rozwiązanie problemu (np09a.cm+np09.inc);
wyjaśnij
na czym polega błąd !
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 system operacyjny 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 20
Zaimplementuj przy pomocy monitora semafor, w którym wstrzymane
procesy nie są wybierane losowo lecz przy pomocy kolejki FIFO
(wykorzystaj priorytety !). Zaprojektuj eksperyment pokazujący że to
faktycznie jest semafor FIFO. Do sprawozdania oprócz kodu dołącz
wydruki ...
Zadanie 21
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 22
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.
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). Patrz także Uwaga 1.
Zadanie 23
(Trzy grupy procesów + pudełko)
Mamy 3 grupy procesów typu "A", "B" i "C", które chcą wejść do pudełka
...
Warunek: w danej chwili w pudełku mogą przebywać procesy tylko 1
grupy
Dodatkowo: chcemy żeby w pudełku przebywało dużo procesów (np
żeby nie były wpuszczane po jednym)
Mechanizmy niskiego poziomu.
Będziemy się teraz zajmować konstruowaniem "narzędzi niskiego
poziomu" do wzajemnego wykluczania ...
Zadanie 24
(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
Wskazówka: 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();
Mogłoby się zdawać, że w algorytmie Petersena wystarczy użyć samej
zmiennej "turn" lub samej zmiennej "flag". Zastanów się, jakie by to
wprowadzało błędy !.
UWAGA: kolejność modyfikowania zmiennych turn i flag[] przed sek kryt
ma duże znaczenie ! Wytłumacz dlaczego wybrałes taką a nie inna
kolejność.
(koniec zadania 24)
Specjalne rozkazy procesora.
Wiadomo, że przełączenie kontekstu może nastąpić między
dwoma
rozkazami procesora (a nie w trakcie wykonywania jednego z nich), tak
więc rozkazy procesora są w pewnym sensie niepodzielne. Przypuśćmy, że
dysponujemy rozkazem procesora TestAndSet, zdefiniowanym
następująco (w języku BACI):
atomic int TestAndSet(int &zmienna)
{ // dzieki "atomic" jest niepodzielna !!
int u;
u = zmienna;
zmienna = 1;
return u;
}
Taki rozkaz może służyć do realizacji wzajemnego wykluczania (przy
pomocy tzw aktywnego czekania):
int wyklucz=0;
void proc(int i)
{
cout<<"proces nr "<<i<<endl;
while(1) {
while(TestAndSet(wyklucz)); // aktywne czekanie
// sekcja krytyczna
wyklucz=0;
// reszta ...
}
}
Zadanie 25
Przeanalizuj działanie wzajemnego wykluczania przy pomocy TestAndSet
(należy opisać jak to działa oraz sprawdzić eksperymentalnie dla 10
procesów). Przygotuj specjalne funkcje do uruchamiania przy wchodzeniu
i wychodzeniu ze sekcji krytycznej o nazwach Prolog() i Epilog().
Proces kontrolny powinien pokazywac ciag 10 bitow mówiacych który z 10
procesów przebywa wewnątrz sekcji krytycznej. Do sprawozdanie wklej
także wydruk generowany przez proces kontrolny.
Zadanie 26
Przypuśćmy, że posiadamy niepodzielną instrukcję procesora Exchange(i,j),
która zamienia wartości zmiennych. Spróbuj zastosować ją do realizacji
wzajemnego wykluczania. Sprawdź, czy wszystko działa poprawnie dla 10
procesów tak jak w poprzednim zadaniu (oczywiście należy także opisać
jak to działa). Pamiętaj o użyciu słowa atomic w definicji
funkcji Exchange!
Własny semafor.
UWAGA: Narzędzia
z powyższych zadań oparte były na aktywnym czekaniu. Oznacza
to, że proces używający takich narzędzi musi być obsługiwany przez
procesor nawet wtedy gdy "nic nie robi", a jedynie czeka na pewne
zdarzenie ...
W poniższym zadaniu należy zastosować
możliwość usypiania/budzenia procesu. Jeśli w systemie istnieją
"działające" i "uśpione" procesy, to procesor jest przydzielany jedynie
działającym, tj przełącza się jedynie między działającymi procesami.
Zazwyczaj nawet na maszynie z jednym procesorem istnieje bardzo wiele
procesów, jednak wszystko działa całkiem sprawnie gdyż większość z
nich "śpi" !
W języku BACI do usypiania/budzenia służą
funkcje :
void suspend(void)
// usypia proces
void revive(int process_number)
// budzi proces o podanym nr
int which_proc()
// zwraca nr bieżącego procesu
Zadanie 30 (*)
Zaimplementuj semafor ogólny (metodami elementarnymi,
"niskopoziomowymi").
Ma to być wersja semafora w której procesy do wznowienia są wybierane z
kolejki FIFO, inaczej niż w przypadku standardowych semaforów j. BACI
gdzie procesy były wybierane losowo.
- Zadanie należy wykonać używając wyłącznie
niskopoziomowych
narzędzi języka BACI (suspend, revive; nie wolno stosować "wbudowanych"
semaforów ani monitorów; zasadniczo nie wolno też używać atomic).
- Wymaga się, aby proces wstrzymany przez wait() znajdował
się w stanie uśpienia.
- Do "wzajemnego wykluczania" należy używać mechanizmów z zadan 25
lub 26 (proces oczekujący na wejście do sekcji krytycznej przez krótki
czas będzie aktywnie czekał - ale to jest dopuszczalne). Slowa "atomic"
wolno używać tylko i wyłącznie do definiowania rozkazów TestAndSet lub
Exchange !
Po zrobieniu tego zadania zastanów się czy Twoja
implementacja na pewno jest poprawna, i czy używając narzędzi j. BACI w
ogóle można taką implementacje wykonać ... |
Nie zapomnij też przetestować swojego semafora np przy pomocy 10
procesów wchodzących do "sek krytycznej" w której mogą przebywać <=3
procesy + proces kontrolny wyświetlający bity.