1 Programowanie wspóªbie»ne wiczenia 12 Przestrzenie krotek cz. 2 Zadanie 1: Przychodnia lekarska W przychodni lekarskiej pracuje L > 0 lekarzy, z których ka»dy ma jedn¡ z 0 < S ≤ L specjalno±ci, przy czym mo»e by¢ wielu lekarzy tej samej specjalno±ci a dla danej specjalno±ci istnieje co najmniej jeden lekarz j¡ posiadaj¡cy. Do przychodni lekarskiej zapisanych jest s¡ identykowani unikalnymi liczbami z przedziaªu 0 . . . P − 1, za± specjalno±ci liczbami z przedziaªu Ka»dy pacjent w p¦tli niesko«czonej zaªatwia zgªasza si¦ do lekarza. 0 . . . L − 1, 0 . . . S − 1. P pacjentów. Lekarze pacjenci liczbami z przedziaªu void wlasneZdroweSprawy(int pid), po czym Pacjent mo»e »yczy¢ sobie wizyty u konkretnego lekarza lub dowolnego lekarza konkretnej specjalno±ci. Ponadto pacjenci mog¡ by¢ uprzywilejowani b¡d¹ zwykli. cjent oczekuje, a» lekarz go przyjmie, po czym odbywa si¦ identykatorem lekarza przyjmuj¡cego pacjenta. Ka»dy lekarz natomiast w p¦tli niesko«czonej void wizyta(int lid), Pa- gdzie lid jest void pijeKawe(int lid) a nast¦pnie przyjmuje pacjenta. W pierwszej kolejno±ci próbuje przyj¡¢ pacjenta uprzywilejowanego, je±li tacy oczekuj¡ na wizyt¦. Je±li nie ma czekaj¡cych pacjentów uprzywilejowanych, to lekarz przyjmuje dowolnego pacjenta (tj. zwykªego lub uprzywilejowanego) lub czeka, je±li nie ma »adnych pacjentów, których mógªby przyj¡¢. Przyj¦cie pacjenta to void badanie(int pid), gdzie pid jest identykatorem badanego pacjenta. W danej chwili lekarz mo»e bada¢ co najwy»ej jednego pacjenta. Lekarz nie mo»e czeka¢, je±li oczekuje pacjent, którego mógªby przyj¡¢. Pacjenci uprzywilejowani mog¡ zagªodzi¢ zwykªych. Celem zadania jest zaimplementowanie procesów lekarza i pacjenta. Do wyboru lekarza przez pacjenta nale»y u»y¢ funkcji void wybor(bool ∗ czyLekarz, int ∗ id, bool ∗ uprzyw). Funkcja in- formuje, czy pacjent wybraª lekarza czy specjalizacj¦ (czyLekarz), jaki jest identykator tego/tej lekarza/specjalizacji (id) i czy pacjent czekaj¡c na wybran¡ wizyt¦ jest uprzywilejowany (uprzyw). process lekarz(int lid , int int pid ; do { sid ) { /∗ identykatory lekarza i specjalno±ci ∗/ pijeKawe(lid ); if ( ! tsTryFetch("ZGLOSZENIE ?d %d %d %d", &pid, true, lid, sid)) tsFetch("ZGLOSZENIE ?d ?d %d %d", &pid, NULL, lid, sid); tsPut("ZAPROSZENIE %d %d", pid, lid); badanie(pid); } while (1); } process pacjent(int pid) { int lsid ; bool czyLekarz, uprzyw; do { /∗ identykator pacjenta ∗/ wlasneZdroweSprawy(pid); wybor(&czyLekarz, &lsid, &uprzyw); if (czyLekarz) /∗ "dziura" na pozycji identykatora specjalno±ci ∗/ tsPut("ZGLOSZENIE else %d %d %d ?d", pid, uprzyw, lsid); /∗ "dziura" na pozycji identykatora lekarza ∗/ tsPut("ZGLOSZENIE %d %d ?d %d", pid, uprzyw, lsid); tsFetch("ZAPROSZENIE %d ?d", pid, &lsid); wizyta( lsid ); 2 while (1); } } W powy»szym rozwi¡zaniu nale»y zwróci¢ uwag¦ na dziurawe krotki. Dla przypomnienia, krotka z dziur¡ na danej pozycji pasuje na tej pozycji do dowolnego wzorca. Zadanie 2: Porównywanie liczb L > 0 liczb caªkowitych, to jest krotek w formacie "LICZBA %d". 1 < N ≤ L procesów, ka»dy z unikalnym identykatorem z przedziaªu 0 . . . N − W przestrzeni krotek znajduje si¦ Do dyspozycji jest 1. Ka»dy z procesów ma za zadanie wybra¢ jedn¡ z liczb (przy czym dwa procesy nie mog¡ wybra¢ tej samej liczby), porówna¢ wybran¡ liczb¦ ze wszystkimi innymi L−1 liczbami, obliczaj¡c ile spo±ród nich jest wi¦kszych od wybranej liczby, a nast¦pnie umie±ci¢ wynik oblicze« w przestrzeni krotek i zako«czy¢ dziaªanie. Celem zadania jest napisanie tre±ci takiego procesu. Mo»na zaªo»y¢, »e po zako«czeniu oblicze« krotki z liczbami nie b¦d¡ ju» potrzebne. process proc(int id) { int moja, tmp, wynik; tsFetch("LICZBA ?d", &moja); tsPut("TMP %d %d", id, moja); for (int i = id + N; i < L; i += N) { tsFetch("LICZBA ?d", &tmp); tsPut("TMP %d %d", i, tmp); } wynik = 0; for (int i = 0; i < L; ++i) { tsRead("TMP %d ?d", i, &tmp); if (tmp > moja) ++wynik; } tsPut("WYNIK %d %d", moja, wynik); } Kluczowym pomysªem w powy»szym rozwi¡zaniu jest zorganizowanie krotek z liczbami tak, aby mo»na byªo efektywnie po nich iterowa¢. Odbywa si¦ to poprzez numerowanie krotek. Takie numerowanie mo»na zaimplementowa¢ na wiele sposobów. Zaprezentowane rozwi¡zanie stara si¦ zbalansowa¢ prac¦ poszczególnych procesów, zakªadaj¡c i» wszystkie one dziaªaj¡ z podobn¡ pr¦dko±ci¡. Zadanie 3: Zliczanie krotek W przestrzeni krotek znajduje si¦ pewna liczba krotek w formacie "KROTKA %d", gdzie "%d" mo»e mie¢ dowoln¡ caªkowit¡ warto±¢. Celem zadania jest napisanie wspóªbie»nego programu licz¡cego Do dyspozycji jest N > 0 procesów, identykowanych unikalnymi liczbami z prze0 . . . N − 1. W rezultacie swojego dziaªania ka»dy proces powinien wypisa¢ liczb¦ krotek "KROTKA %d" w przestrzeni i zako«czy¢ dziaªanie. Nale»y przyj¡¢, »e po zako«czeniu ostatniego procesu wszystkie krotki "KROTKA %d" powinny nadal znajdowa¢ si¦ w przestrzeni krotek. Nate krotki. dziaªu tomiast jakiekolwiek inne krotki wytworzone przez procesy mog¡ pozosta¢ w przestrzeni jedynie je±li ich liczba jest staªa. process zliczacz(int int n = 0; int v, c ; id) { /∗ zainicjalizuj obliczanie ∗/ if (id == 0) { tsPut("WYNIK d %d", 0, 0); 3 } /∗ policz lokalnie tyle krotek ile mo»esz ∗/ while (tsTryFetch("KROTKA ++n; tsPut("TMP %d", ?d", &v)) { v); } /∗ dodaj swój wynik do wyników reszty ∗/ tsFetch("WYNIK ?d ?d", &c, &v); tsPut("WYNIK %d %d", c + 1, v + n); tsRead("WYNIK %d ?d", N, &n); /∗ przywró¢ oryginalne krotki ∗/ while (tsTryFetch("TMP ?d", &v)) tsPut("KROTKA %d", v); printf ( "WYNIK PROCESU %d = %d\n", id, n); } W powy»szym rozwi¡zaniu, w liczeniu bior¡ udziaª wszystkie procesy (w miar¦ mo»liwo±ci). Najpierw wyci¡gaj¡ one z przestrzeni krotek wszystkie krotki. Ka»dy proces liczy lokalnie, ile krotek udaªo mu si¦ wyci¡gn¡¢, otrzymuj¡c lokalny wynik cz¦±ciowy. Nast¦pnie te wyniki cz¦±ciowe s¡ sumowane dla wszystkich procesów, co daje sumaryczn¡ liczb¦ krotek w przestrzeni. Nale»y tak»e zwróci¢ uwag¦ na instrukcj¦ tsRead. Wstrzymuje ona wszystkie procesy do momentu, gdy wszystkie wyniki cz¦±ciowe zostaªy dodane do wyniku globalnego. Sumowanie wyników cz¦±ciowych w zaprezentowanym rozwi¡zaniu odbywa si¦ w sekcji krytycznej proces najpierw wyci¡ga krotk¦ z wynikiem globalnym z przestrzeni, dodaje do wyniku globalnego swój wynik lokalny, a nast¦pnie umieszcza zwi¦kszony wynik globalny ponownie w przestrzeni. Kolejne procesy modykuj¡ wi¦c wynik globalny sekwencyjnie. Innymi sªowy, czas obliczania wyniku globalnego maj¡c dane wyniki lokalne jest proporcjonalny do liczby procesów, co mo»e by¢ nieefektywne. Poni»sze rozwi¡zanie to poprawia. process zliczacz2(int id) int n = 0; int v; { /∗ policz lokalnie tyle liczb ile mo»esz ∗/ while (tsTryFetch("KROTKA ++n; tsPut("TMP %d", ?d", &v)) { v); } /∗ globalna redukcja sumy lokalnych wyników ∗/ for (int skok = 1; id + skok < N; skok ∗= 2) { if (id % (2 ∗ skok) != 0) break; tsFetch("WYNIK %d ?d", id + skok, &v); n += v; } tsPut("WYNIK %d %d", id, n); /∗ wczytaj globalny wynik ∗/ tsRead("WYNIK %d ?d", 0, &n); /∗ przywró¢ oryginalne liczby ∗/ while (tsTryFetch("TMP ?d", &v)) tsPut("KROTKA %d", v); printf ( "WYNIK PROCESU %d = %d\n", id, n); } W ulepszonym rozwi¡zaniu sumowanie wyników cz¦±ciowych odbywa si¦ w czasie proporcjonalnym do logarytmu z liczby procesów. Ostateczny wynik globalny produkowany jest przez proces o indeksie 0, przez co zmienia si¦ tak»e nieznacznie instrukcja wstrzymuj¡ca tsRead. Warto tak»e 4 zauwa»y¢, i» w ulepszonym rozwi¡zaniu sumaryczna liczba instrukcji na krotkach "WYNIK %d %d %d" jest mniejsza jedynie o 1 od sumarycznej liczby instrukcji na tych krotkach w rozwi¡zaniu poprzednim. Efektywno±¢ ulepszonego rozwi¡zania bierze si¦ st¡d, i» u»ywa ono wielu takich krotek, przez co dost¦p do nich mo»e odbywa¢ si¦ wspóªbie»nie. Zadanie 4: Od±miecanie krotek (je±li starczy czasu) Celem zadania jest wykorzystanie poprzedniego rozwi¡zania do zaimplementowania efektywnej, samo-od±miecaj¡cej si¦ bariery wykonywanej przez N > 0 procesów indeksowanych od 0 do N − 1. Genez¡ problemu jest fakt, i» niektóre programy mog¡ pozostawia¢ w przestrzeni krotki, które ju» nigdy nie zostan¡ u»yte ±mieci. Dla przypomnienia, bariera to operacja wykonywana przez wszystkie N procesów w ten sposób, i» »aden proces nie mo»e zako«czy¢ jej wykonywania o ile wszystkie procesy nie rozpocz¦ªy jej wykonywania. Maj¡c dan¡ instrukcj¦ bariery, od±miecanie nieu»ywanych krotek mo»e by¢ zaimplementowane nast¦puj¡co po zako«czeniu oblicze« generuj¡cych takie krotki procesy wykonuj¡ instrukcj¦ bariery, po której nast¦puje od±miecanie tsTryFetch, dopóki true dla danego wzorca, i tak dalej dla kolejnych wygenerowanych wcze±niej wzorców wygenerowanych krotek przy wykorzystaniu przez ka»dy proces instrukcji zwraca ona krotek. Mamy wtedy gwarancj¦, »e od±miecane krotki nie b¦d¡ ju» potrzebne. Baz¡ dla bariery mo»e by¢ poprzednie rozwi¡zanie, jak poni»ej. void bariera(int id) { for (int skok = 1; id + skok < N; skok ∗= 2) { if (id % (2 ∗ skok) != 0) break; tsFetch("BARIERA %d", id + skok); } tsPut("BARIERA %d", id); tsRead("BARIERA %d", 0); } Rozwi¡zanie opiera si¦ na hierarchicznym, logarytmicznym czekaniu a» wszystkie procesy dotr¡ do bariery. Jako ostatni dociera proces To rozwi¡zanie ma dwie wady. wywoªaniu, krotka "BARIERA 0" 0, st¡d instrukcja wstrzymuj¡ca tsRead. Po pierwsze, b¦dzie ono dziaªaªo jedynie raz. Przy drugim b¦dzie ju» w przestrzeni, wi¦c instrukcja wstrzymuj¡ca si¦ nie zablokuje. Po drugie, usuni¦cie tej krotki z przestrzeni nie jest takie proste. Dokªadniej, musimy mie¢ gwarancj¦, »e ju» »aden inny proces nie b¦dzie jej potrzebowaª. To sugeruje, i» powinni±my mie¢ kolejn¡ barier¦. Poprawione rozwi¡zanie rozwi¡zuje te dwa problemy nast¦puj¡co. Po pierwszy, bariery maj¡ swoje numery (epoki) ka»de wywoªanie funkcji bariery zwi¦ksza numer epoki. Po drugie, kolejne wywoªanie bariery usuwa (w odpowiednim momencie) krotki bariery z poprzedniej epoki. void bariera2(int id) { static int epoka = 0; for (int skok = 1; id + skok < N; skok ∗= 2) { if (id % (2 ∗ skok) != 0) break; tsFetch("BARIERA %d %d", epoka, id + skok); } tsPut("BARIERA %d %d", epoka, id); tsRead("BARIERA %d %d", epoka, 0); if (id == 0) tsTryFetch("BARIERA %d %d", epoka − 1, 0); } W tym rozwi¡zaniu, funkcja bariery mo»e by¢ woªana wielokrotnie. Po ka»dym jej zako«czeniu przez wszystkie procesy, w przestrzeni krotek pozostanie dokªadnie jedna krotka "BARIERA %d %d" liczba nieod±mieconych krotek bariery b¦dzie staªa, niezale»nie od tego, ile razy funkcja bariery byªa poprzednio woªana. Niemniej jednak takie rozwi¡zanie mo»e by¢ nadal niezadowalaj¡ce. 5 Poni»sze rozwi¡zanie eliminuje t¦ wad¦, zwi¦kszaj¡c dwukrotnie (ale nie asymptotycznie) zªo»ono±¢ operacji bariery. void bariera3(int id) { for (int faza = 1; faza <= 2; ++faza) { for (int skok = 1; id + skok < N; skok ∗= 2) { if (id % (2 ∗ skok) != 0) break; tsFetch("BARIERA %d %d", faza, id + skok); } if (faza == 1) { tsPut("BARIERA %d %d", faza, id); tsRead("BARIERA %d %d", faza, 0); } else { if (id != 0) tsPut("BARIERA %d %d", faza, id); else tsFetch("BARIERA %d %d", faza − 1, 0); } } } Rozwi¡zanie opiera si¦ na dwóch fazach. Je±li jakikolwiek proces rozpoczyna faz¦ drug¡, to wszystkie procesy, wª¡czaj¡c w to proces 0, wykonaªy ju» instrukcj¦ zapewniona jest wªa±ciwo±¢ bariery. Je±li natomiast proces 0 tsPut z fazy pierwszej w fazie drugiej wykonuje ostatni¡ tsFetch dla krotki wygenerowanej w fazie pierwszej, to wszystkie pozostaªe procesy tsPut z fazy drugiej, zako«czyªy wi¦c faz¦ pierwsz¡, a zatem usuwana przez proces 0 krotka z fazy pierwszej nie b¦dzie ju» potrzebna »adnemu procesowi do tsRead z fazy pierwszej. instrukcj¦ wykonaªy Innymi sªowy, po zako«czeniu funkcji przez proces "BARIERA %d %d". 0 w przestrzeni nie zostanie »adna krotka