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:

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) :

 

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).
 
Oprogramowanie BACI (DOS):  
  • Kompilator BACI (bacc.exe)
  • Interpreter BACI (bainterp.exe)
  • DisAssembler BACI (badis.exe)
  • Dokumentacja (baci.ps)
  • Dokumentacja (baci.pdf)
  • Krótki opis (baci.htm)
  • C-podobny

  • Kompilator BACI (bapas.exe)
  • Dokumentacja (guidepas.pdf)
  • Pascalo-podobny

     
    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" :

    powoduje utworzenie pliku z PCODE-m. Uruchomienie interpretera PCODE-u : 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) :

    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():

    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:


    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ć:
    1. opis błędu + wydruk pokazujący błąd
    2. 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:

     
    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:

    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:

    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":

    Definicja Monitora.
    Monitor jest zbiorem procedur i zmiennych, spełniającym następujące warunki:

    1. 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.
    2. W danej chwili tylko jeden proces może przebywać w monitorze (tj wykonywać jego procedurę), co umożliwia prostą realizację wzajemnego wykluczania.
    3. 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.
    4. 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.
    5. 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()".
    6. 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)".
    7. 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.
    8. 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:

    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:

    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 :

    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.
    1. 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).
    2. Wymaga się, aby proces wstrzymany przez wait() znajdował się w stanie uśpienia.
    3. 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.