Systemy operacyjne. Zajęcia 11. Monitory 1. Monitor to mechanizm zamykający całą pamięć wspólną i synchronizację w pojedynczym module. 2. Monitor posiada całą pamięć wspólną jako część 'prywatną': dostępu do niej nie mają taski (czyli typowo wątki) użytkownika bezpośrednio a jedynie przez procedury wejściowe (entries, entry procedures). Procedury te mogą być wywoływane przez taski użytkownika i mogą przyjmować argumenty i zwracać wartości. Jeśli w danym momencie jakiś wątek jest w trakcie wykonywania kodu którejś z procedur wejściowych, to mówimy że jest on w monitorze. 3. W monitorze (czyli w którejś z procedur wejściowych) może być tylko jeden task na raz. Jeśli monitor jest zajęty (czyli ktoś wykonuje jakąś procedurę wejściową) to zadania użytkownika są kolejkowane w kolejce wejściowej (entry queue). Po wyjściu zadania z monitora (czyli po zakończeniu wykonywania procedury wejściowej) monitor przekazywany jest następnemu zadaniu z kolejki. W ten sposób monitor zapewnia zadaniom wzajemne wykluczanie (mutual exclusion), chroniąc tym samym pamięć wspólną. Rozwiązania problemów programowania współbieżnego z wykorzystaniem monitora są z reguły prostsze i mniej 'trickowe' niż z wykorzystaniem semaforów. Niestety, z monitorami są związane pewne problemy. W Javie monitor może być realizowany poprzez klasę ReentrantLock implementującą interfejs Lock. Tworzymy obiekt: Lock monitor = new ReentrantLock(true); //true będzie kolejka Treść procedury wejściowej to dowolny kod znajdujący się wewnątrz bloku: monitor.lock(); //tu kod monitor.unlock();
4. Oprócz tego monitor ma tzw. mechanizm kolejek wewnętrznych zwanych też zmiennymi warunkowymi (queues, condition variables). W każdym monitorze można stworzyć dowolną ilość kolejek wewnętrznych. Kolejka taka ma trzy dostępne operacje (można je wykonywać tylko wewnątrz procedur wejściowych, bo kolejki wewnętrzne są widoczne tylko wewnątrz monitora): q.empty() - true jeśli kolejka jest pusta i false w.p.p. q.delay() - zawieś się i wpisz do kolejki q, zwalnia task czekający w kolejce wejściowej q.continue() - jeśli q jest pusta nic nie rób, w przeciwnym razie odwieś pierwsze z zadań czekających w kolejce q Czasem można się także spotkać z operacją: q.continueall() - odwieś wszystkie taski czekające w q, W Javie kolejka wewnętrzna jest reprezentowana przez interfejs Condition i tworzona przy pomocy metody: Condition q = monitor.newcondition(); Odpowiednikiem operacji delay jest metoda await(): q.await(); q.awaituninterruptibly(); Z kolei odpowiednikiem operacji continue jest metoda signal() q.signal(); Wreszcie odpowiednikiem operacji continueall jest metoda signalall() q.signalall(); Źródło rysunku: http://www.cs.mtu.edu/~shene/nsf-3/e-book/monitor/cv.html
Działanie operacji delay W1 W2 wchodzi do monitora chce wejść do monitora: czeka w kolejce wejściowej... delay (zawiesza sie). ---------------->.. może wejść do monitora... wychodzi z monitora (kończy procedurę wejściową). Jest kilka semantyk działania continue: W1 W2 W3 wchodzi do monitora q.delay. wchodzi do monitora.. chce wejść do monitora. q.continue. ma się odwiesić chce się wykonywać chce wejść (w monitorze) (w monitorze) (do monitora) A) Semantyka Hoare'a (oryginalnie zaproponowana w 1974 przez Hoare'a, który był twórcą idei monitora). Zadanie które wykonało q.continue zawiesza się (w dodatkowej kolejce tymczasowej). Odwieszane jest zadanie z kolejki q. Wykonuje się ono w monitorze do końca swojej procedury wejściowej. Po tym jak wyjdzie z monitora, odwieszane jest zadanie z kolejki tymczasowej. Jeśli zanim wyjdzie zrobi continue kolejka tymczasowa rośnie. B) Semantyka Mesy (od języka programowanie Mesa, w którym ją wprowadzono jest to semantyka współcześnie typowo używana, także w Javie, jest nazywana także semantyką wait - notify). Zadanie, które wykonało q.continue wykonuje się dalej do końca swojej procedury wejściowej. Pierwsze zadanie z kolejki q wznawia działanie w monitorze po wyjściu tego zadania z monitora. W przypadku continueall, zadania z kolejki wykonują się w monitorze po kolei. C) Często przyjmuje się (aby uniknąć problemu), że continue może być tylko ostatnią operacją w kodzie procedury wejściowej.
5. Przykłady: A) Semafor: q: queue; n: int :=x; entry P() if n = 0 q.delay(); else n--; entry V() if not q.empty() q.continue(); // może być samo q.continue() bo na kolejce pustej nic by się nie działo else n++; ; Zadanie: jaki to jest semafor? Binarny? Liczbowy? Ograniczony? Nieograniczony? B) Producent i konsument (z buforem jednoelementowym): full: boolean = false; bufor: qp: queue; //kolejka producentow qk: queue; //kolejka konsumentow entry wloz(in x) if full qp.delay(); **** bufor = x; full:= true; if not qk.empty() qk.continue(); entry wez(out x) if!full qk.delay(); **** x = bufor; full:= false; if not qp.empty() qp.continue(); Zadanie (tablicowe): zrobić producenta i konsumenta z buforem o dowolnym rozmiarze. Uwaga: Bufor, jako pamięć wspólna jest wewnątrz monitora. Zadanie: Zaimplementować to w Javie: w programie powinien być wątek producenta, watek konsumenta, klasa monitora i main. W mainie stworzyć monitor, kilku producentów, kilku konsumentów, przekazać im monitor. Producent powinien produkować Stringa będącego katenacją numeru wątku i numeru kolejnego wyprodukowania. Bufor może być listą (w konstruktorze dla monitora można podać maksymalny rozmiar).
Uwaga: w Javie w poniższej sytuacji W1 W2 W3 monitor.lock() q.await(). monitor.lock(). monitor.lock(). q.signal(). ma się odwiesić chce się wykonywać chce wejść (w monitorze) (w monitorze) (do monitora) nie ma gwarancji, że wątek W1 dostanie się do monitora przed wątkiem W3. Zatem, jeśli W2 to producent, który umieścił dane w buforze, nie ma gwarancji, że konsumentowi W1, który ma tą daną dostać, nie podbierze jej W3. Wówczas dojdzie do błędu polegającego na tym, że W1 i W3 pobiorą to samo (albo W1 skonsumuje daną a W3 mimo że się odwiesi, dostanie pustą listę). Rozwiązaniem jest zastąpienie w linijkach oznaczonych w powyższym kodzie gwiazdkami ifów przez while. Dodatkowo w Javie jest możliwy tzw 'spurious wakeup'. Oznacza to, że wątek, czekający na q.await() może przebudzić się bez wykonania signal. http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/condition.html Należy więc zawsze warunek przebudzenia sprawdzać w pętli while. Pytanie: czy wobec możliwości zawłaszczenia monitora przez zadanie wchodzące do niego oraz możliwość wystąpienia 'spurious wakeup' jest możliwe rozwiązanie sprawiedliwe? Odp. Tak. Idea rozwiązania: dodać dodatkową kolejkę (nie służącą do blokowania wątków tylko zwykłą kolejkę) obiektów typu Thread. Wątek zawieszany dodaje wpis do kolejki Thread. Jeśli przy wchodzeniu do monitora ta kolejka jest niepusta i nie jest spełniony warunek możliwości wyprodukowania/skonsumowania to producent/konsument powinni się zawiesić w swoich kolejkach (i dodać swoje wątki do kolejki Thread). Po odwieszeniu wątek powinien sprawdzić, czy jego obiekt wątek jest na początku kolejki Thread i, jeśli tak jest (i warunek wejścia do monitora jest spełniony tzn nie jest to spurious wakeup) to wątek usuwa swój wpis z początku kolejki Thread i idzie dalej do monitora. Uwaga: w Javie sprawdzanie bieżącego wątka to Thread.CurrentThread();
C) Czytelnicy i pisarze (z preferencjami): qc, qp: queue; //kolejka czytelnikow i kolejka pisarzy r:=0, w:=0: integer; entry PC() //poczatek czytania if w = 1 or not qp.empty() qc.delay(); r++; if not qc.empty() qc.continue(); //kaskadowe odwieszanie czytelnikow entry PP()//poczatek pisania if w = 1 or r > 0 qp.delay(); w ++; entry KC()//koniec czytania r --; if r = 0 qp.continue(); entry KP() //koniec pisania w--; if not qc.empty() qc.continue(); else qp.continue(); ; Manipulując fragmentami kodu zaznaczonymi na żółto można sterować priorytetami. Uwaga: Ponieważ czytelnicy czytają współbieżnie, czytanie (i pisanie) nie może odbywać się w monitorze. Zadanie: zaimplementować to w Javie, czytanie i pisanie ma być atrapą. Zadanie na BaCy dotyczy wersji FIFO. Rozwiązanie (zabezpieczające nas przed 'spurious wakeup') i realizujące FIFO znajduje się w prezentacji http://www.cs.chalmers.se/cs/grundutb/kurser/ppxt/ht2007/lectures/5x6.pdf slajdy 27-32
6. Symulacja monitora za pomocą semaforów (monitor i semafor są sobie równoważne): SemaforBinarny mutex:=otwarty; //kolejka wejściowa Dla każdej kolejki wewnętrznej q: int czekaja := 0 ; SemaforBinarny s:=zamkniety; Operacje w monitorze entry P() q.delay(); q.continue(); ; Operacje semaforowe <---------> mutex.p() <---------> mutex.v() <---------> czekaja++; mutex.v(); S.P(); czekają--; <---------> if (czekaja>0) s.v(); else mutex.v(); Po q.continue pierwszym zadaniem, które ma się wykonywać jest to, które było zawieszone, dlatego nie otwieram mutexa (żeby nie weszły zadania z kolejki wejściowej). Do monitora wejdzie zadanie, które zrobiło delay i 'przejmie' mutexa.