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) :
| |
C-podobny |
| |
Pascalo-podobny |
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 np01powoduje utworzenie pliku z PCODE-m. Uruchomienie interpretera PCODE-u :
bainterp np01każ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 !!!. |
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 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 tymDo 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 ...).
// 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;
}
}
..................................................................
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:
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
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:
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
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:
void Producent() |
void Konsument() |
Zadanie 6
Zadanie 8
1 0 0 | 0 0 0 0 0 0 0 0 0 0Dodatkowo wyjaśnij działanie algorytmu przedstawionego w przykładzie np05.cm (wyjaśnij rolę semaforów W i R).
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
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
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
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:
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
Zadanie 22
| 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
Będziemy się teraz zajmować konstruowaniem "narzędzi niskiego
poziomu" do wzajemnego wykluczania ...
Zadanie 24
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.
// 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)
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
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
| 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.