Analiza programowych rozwiązań problemu wzajemnego wykluczania

Z PUTWiki
Skocz do: nawigacji, wyszukiwania

Spis treści

[edytuj] Formalne sformułowanie problemu

Dany jest zbiór procesów sekwencyjnych komunikujących się przez wspólną pamięć. Każdy z procesów zawiera sekcję krytyczną, w której następuje dostęp do wspólnej pamięci. Procesy te są procesami cyklicznymi

Zakłada się ponadto:

  1. Zapis i odczyt wspólnych danych jest operacją niepodzielną, a próba jednoczesnych zapisów lub odczytów realizowana jest sekwencyjnie w nieznanej kolejności.
  2. Sekcje krytyczne nie mają priorytetu.
  3. Względne prędkości wykonywania procesów są nieznane.
  4. Proces może zostać zawieszony poza sekcją krytyczną.
  5. Procesy realizujące instrukcje poza sekcją krytyczną nie mogą uniemożliwiać innym procesom wejścia do sekcji krytycznej. Inaczej słaby warunek postępu.
  6. Procesy powinny uzyskać dostęp do sekcji krytycznej w skończonym czasie. Inaczej mocny warunek postępu.

Przy tych założeniach należy zagwarantować, że w każdej chwili czasu co najwyżej jeden proces jest w swej sekcji krytycznej. Nazywa sie to warunkiem bezpieczeństwa.

[edytuj] Algorytm naiwny nr 1

 
01 program VERSIONONE;
 
02 var processNumber: INTEGER;
03 procedure PROCESSONE;
04 begin
05   while True do
06    begin
07      while processNumber=2 do;
08      criticalSectionOne;
09      processNumber:=2;
10      otherStuffOne;
11    end;
12 end;
 
13 procedure PROCESSTWO;
14 begin
15   while True do
16      begin
17        while processNumber=1 do;
18        criticalSectionTwo;
19        processNumber:=1;
20        otherStuffTwo;
21      end;
22 end;
 
23 begin
24      processNumber:= 1;
25      parbegin
26         PROCESSONE;
27         PROCESSTWO;
28      parend;
29 end.
 

Procesy sekwencyjnie wchodzą do sekcji jeden po drugim (nie ma możliwości aby jeden proces wszedł do sekcji dwa razy pod rząd). O tym, który proces może wejść decyduje zmienna processNumber na której odbywa się aktywne czekanie w liniach 17 i 07.

[edytuj] Problemy

Procesy dokonują ścisłej wymiany - jeżeli jeden z nich się zawiesi, drugi nie będzie mógł ponownie wejść do sekcji krytycznej. Powiedzmy, że proces pierwszy po wyjściu z sekcji krytycznej zawiesza się podczas wykonywania otherStuffOne (wg. punktu 4. może tak się zdarzyć). Proces drugi wchodzi do sekcji krytycznej, wychodzi z niej przestawiając processNumber na 1, a następnie czeka w nieskończoność, aż proces pierwszy przestawi ją znów na 2. Niespełnione są więc punkty 5 i 6 - słaby oraz silny warunek postępu. Identyczna sytuacja będzie miała miejsce, jeżeli jeden z procesów zakończy swoje działanie.

[edytuj] Algorytm naiwny nr 2

 
01 program VERSIONTWO;
 
02 var P1inside, P2inside: BOOLEAN;
 
03 procedure PROCESSONE;
04 begin
05   while True do
06      begin
07        while P2inside do;
08        P1inside:=True;
09        criticalSectionOne;
10        P1inside:=False;
11        otherStaffOne;
12      end;
13 end;
 
14 procedure PROCESSTWO;
15 begin
16   while True do
17      begin
18        while P1inside do;
19        P2inside:=True;
20        criticalSectionTwo;
21        P2inside:=False;
22        otherStaffTwo;
23      end;
24 end;
 
25 begin
26   P1inside:=False;
27   P2inside:=False;
28   parbegin
29       PROCESSONE;
30       PROCESSTWO;
31   parend;
32 end.
 

Procesy informują siebie nawzajem o tym, że weszły do sekcji krytycznej przy pomocy zmiennych P1inside i P2inside. Proces zanim wejdzie do sekcji krytycznej sprawdza czy drugi proces już tam nie jest (aktywne czekanie w linijkach 07 i 18).

[edytuj] Problemy

Operacje sprawadzania i ustawiania zmiennych mówiacych o obecności innego procesu w sekcji krytycznej nie są atomowe. Możemy wyobrazić sobie sytuacje gdy obie zmienne P1inside i P2inside ustawione są na false, następnie wykonywana jest linijka 07, a tuż po niej linijka 18. Oba procesy wejdą w takiej sytuacji do sekcji krytycznej w tym samym momencie.

[edytuj] Algorytm naiwny nr 3

 
01 program VERSIONTHREE;
 
02 var P1WantsToEnter: BOOLEAN;
03 var P2WantsToEnter: BOOLEAN;
 
04 procedure PROCESSONE;
05 begin
06   while True do
07      begin
08        P1WantsToEnter:=True;
09        while P2WantsToEnter do;
10        criticalSectionOne;
11        P1WantsToEnter:=False;
12        otherStuffOne;
13      end;
14 end;
 
15 procedure PROCESSTWO;
16 begin
17   while True do
18      begin
19        P2WantsToEnter:=True;
20        while P1WantsToEnter do;
21        criticalSectionTwo;
22        P2WantsToEnter:=False;
23        otherStuffTwo;
24      end;
25 end;
26 begin
27   P1WantsToEnter:=False;
28   P2WantsToEnter:=False;
29   parbegin
30       PROCESSONE;
31       PROCESSTWO;
32   parend;
33 end.
 

Oba procesy deklarują chęć wejścia do sekcji krytycznej przy pomocy zmiennych P1WantsToEnter i P2WantsToEnter po czym sprawdzają, czy inny proces też nie wyraża takiej chęci - jeżeli nie, wchodzą do sekcji krytycznej.

[edytuj] Problemy

Zadeklarowanie chęci wejścia do sekcji i sprawdzenie, czy inny proces nie chce wejść, nie jest atomowe. Można wyobrazić sobie sytuację, w której przy początkowych wartościach false obu zmiennych wykonywane są kolejno linie 08 (pierwszy deklaruje chęć wejścia) i 19 (drugi robi to samo) po czym oba procesy wpadają w nieskończone pętle na liniach 09 i 20. Następuje zakleszcze, więc nie zostaje spełniony warunek 6 (skończony czas dostępu).

[edytuj] Algorytm naiwny nr 4

 
01 program VERSIONFOUR;
 
02 var P1WantsToEnter: BOOLEAN;
03 var P2WantsToEnter: BOOLEAN;
 
04 procedure PROCESSONE;
05 begin
06   while True do
07    begin
08     P1WantsToEnter:=True;
09     while P2WantsToEnter do
10      begin
11        P1WantsToEnter:=False;
12        delay(random,freecycles);
13        P1WantsToEnter:=True;
14      end;
15     criticalSectionOne;
16     P1WantsToEnter:=False;
17     otherStuffOne;
18    end;
19 end;
 
20 procedure PROCESSTWO;
21 begin
22   while True do
23    begin
24      P2WantsToEnter:=True;
25      while P1WantsToEnter do
26       begin
27        P2WantsToEnter:=False;
28        delay(random,freecycles);
29        P2WantsToEnter:=True;
30       end;
31      criticalSectionTwo;
32      P2WantsToEnter:=False;
33      otherStuffTwo;
34    end;
35 end;
 
36 begin
37      P1WantsToEnter:=False;
38      P2WantsToEnter:=False;
39      parbegin
40         PROCESSONE;
41         PROCESSTWO;
42      parend;
43 end.
 

Jest to zmodyfikowana wersja poprzedniego algorytmu polegająca na tym, że podczas aktywnego czekania, aż drugi proces zrezygnuje z ubiegania się o sekcję krytyczną, następuje rezygnacja z ubiegania się o sekcję krytyczną i uśpienie na losowy okres czasu (linie 09-14 i 25-30).

[edytuj] Problemy

Zastosowane zmiany mają przeciwdziałać zakleszczeniom, powodując, że w krytycznym przypadku przedstawionym w poprzednim algorytmie jeden proces zwolni zasoby na losowy okres czasu, a drugi w tym czasie wskoczy na jego miejsce. Problem polega na tym, że istnieje niezerowe prawdopodobieństwo, że oba procesy będą usypiane w tych samych momentach na ten sam okres czasu i nadal będą trwały w zakleszczeniu. Prawdopodobieństwo to maleje wraz ze wzrostem przedziału czasu, z którego losujemy czas "drzemki". Wydłużenie tego czasu do nieskończoności daje nam nadal złamanie warunku 6.

Możliwa jest też sytuacja, w której drugi proces czeka, aż pierwszy skończy, podczas gdy drugi w nieskończoność "wskakuje" w "drzemkę" drugiego procesu głodząc go.

[edytuj] Algorytm Hymana

 
01 var flaga: array[1..2] of integer; (* początkowo 0 *)
02 	    numer: 1..2;
 
03 repeat
04   flaga[i]:=numer;
05   while numer<>i do
06     begin
07       while flaga[j]<>0 do nic;
08       numer:=i;
09     end;
10   criticalSection;
11   flaga[i]:=0;
12   otherStuff;
13 until false;
 

Podejście zaproponowane przez Hymana - nie spełnia jednak warunku bezpieczeństwa.

[edytuj] Problemy

Poniższa sekwencja doprowadza oba procesy do jednoczesnego wejscia do sekcji krytycznej.

flaga[1] = flaga[2] = 0; numer = 2
Proces nr 1 (i = 1) Proces nr 2 (i = 2)
04 flaga[1] = 2 ...
05 while numer<>1 do
06 begin
07 while flaga[2]<>0 do nic;
...
04 flaga[2] = 2
05 while numer<>2 do
10 criticalSection;
08 numer:=1;
10 criticalSection;

[edytuj] Algorytm Dekkera

 
01 program DEKKERALGORITHM;
 
02 var favoredProcess: enum (First, Second);
03 var P1WantsToEnter, P2WantsToEnter: BOOLEAN;
 
04 procedure PROCESSONE;
05 begin
06   while True do
07      begin
08        P1WantsToEnter:=True;
09        while P2WantsToEnter do
10           if favoredProcess=Second then
11             begin
12                P1WantsToEnter:=False;
13                while favoredProcess=Second do;
14                P1WantsToEnter:=True;
15             end;
16        criticalSectionOne;
17        favoredProcess:=Second;
18        P1WantsToEnter:=False;
19        otherStuffOne;
20      end;
21 end;
 
22 procedure PROCESSTWO;
23 begin
24   while True do
25     begin
26        P2WantsToEnter:= True;
27        while P1WantsToEnter do
28          if favoredProcess=First then
29             begin
30               P2WantsToEnter:=False;
31               while favoredProcess=First do;
32               P2WantsToEnter:= True;
33             end;
34        criticalSectionTwo;
35        favoredProcess:=First;
36        P2WantsToEnter:=False;
37        otherStuffTwo;
38      end;
39 end;
 
40 begin
41    P1WantsToEnter:= False;
42    P2WantsToEnter:= False;
43    favoredProcess:= First;
44    parbegin
45        PROCESSONE;
46        PROCESSTWO;
47    parend;
48 end.
 

Jest to rozwinięcie ostatniego algorytmu, w którym w specjalny sposób traktuje się sytuację, gdy dwa procesy naraz zgłosiły chęć wejścia do sekcji krytycznej. Zmienna favoredProcess określa, którego procesu jest właśnie kolej. Jeżeli nasza to czekamy, aż drugi proces zmieni zmienną P2WantsToEnter. W przeciwnym wypadku zwalniamy swoją zmienną P1WantsToEnter i czekamy na naszą kolej. Uzyskujemy naprzemienny dostęp w krytycznych przypadkach jednoczesnej próby dostępu i to bez zaawansowanych narzędzi, takich jak semafory.

Dzięki rezerwowaniu zasobów zapewniamy, że oba procesy nie wejdą na raz do sekcji krytycznej. favoredProcess zapewnia, że każdy proces kiedyś wejdzie. Dzięki temu, że procesy nie czekają ciągle rezerwując zasoby, nie dojdzie do zakleszczenia.

[edytuj] Algorytm Dijkstry

 
01 program DIJKSTRAALGORITHM;
 
02 begin
03  shared
04    flag[1..n]: 0..2;
05    turn       : 1..n;
06  local
07    testi : 0..2;
08    k, otheri, tempi: 1..n;
 
09  while True do
10    begin
 
11       L: flag[i]:=1;
12       otheri:=turn;
13       while otheri≠i do
14         begin
15            testi:=flag[otheri];
16            if testi=0 then
17              turn:=i;
18            otheri:=turn;
19         end;
 
20       flag[i]:=2;
21       for k:=1 to n do
22         if k≠i then
23            begin
24              testi:=flag[k];
25              if testi=2 then
26                 goto L;
27            end;
28       criticalSection;
 
29       flag[i]:=0;
30       otherStuff;
31  end;
32 end.
 

Algorytm składa się z dwóch faz. W pierwszej sprawdzamy, czy jest nasza kolej. Jeżeli nie, a proces, którego jest kolej, wyszedł z sekcji krytycznej (flag[turn]=0), to ustawiamy kolej na siebie. W drugiej fazie sprawdzamy, czy którykolwiek inny proces nie jest w drugiej fazie. Jeżeli tak, to wracamy do fazy pierwszej. W przeciwnym wypadku wchodzimy do sekcji krytycznej.

[edytuj] Problemy

Algorytm ten nie spełnia silnego warunku postępu. Mianowicie przy pewnych prędkościach wykonywania procesów, pewien proces, który przebrnął przez sekcje krytyczną może z powrotem do niej trafić, nie wpuszczając "oczekujących" pozostałych procesów. Ma to miejsce wtedy, jeżeli czekające procesy nie zdążą "zauważyć" zmiany flag[i] na 0). Taka sytuacja może się powtarzać cyklicznie, czyli czekające procesy zostaną "zagłodzone".

[edytuj] Algorytm Petersona dla 2 procesów

 
01 program PATERSONALGORITHM;
 
02 begin
03   shared
04      flag[0..1]: BOOLEAN;
05      turn: INTEGER;
06   local
07      otheri: BOOLEAN;
08      whosei: INTEGER;
 
09   while True do
10      begin
11        flag[i]:= True;
12        turn:=1-i;
13        repeat
14           whosei:=turn;
15           otheri:=flag[1-i];
16        until (whosei=i or not otheri);
17        criticalSection;
18        flag[i]:= False;
19        remainderSection;
20      end;
21 end.
 

Parametr i przyjmuje wartości 0 lub 1 (w zależności od procesu). W algorytmie najpierw w liniach 11 i 12 ustawiamy chęć wejścia do sekcji krytycznej i kolej (turn) na przeciwny proces. Następnie czekamy, aż nie nastąpi nasza kolej albo drugi proces nie będzie wyrażał chęci wejścia do sekcji krytycznej. Dzięki zmiennej turn w przypadku jednoczesnej próby wejścia do sekcji krytycznej zawsze zostanie wybrany tylko jeden proces.

[edytuj] Algorytm Petersona dla n procesów

 
01 program PATERSONALGORITHM_N;
 
02 begin
03   shared
04     flag[1..n]: INTEGER;
05     turn[1..n-1]: INTEGER;
06   local
07     k, l, otheri, whosei: INTEGER;
 
08   while True do
09    begin
10     for k:=1 to n-1 do
11       begin
12        flag[i]:=k;
13        turn[k]:=i;
14        repeat
15           whosei:=turn[k];
16           if whosei≠i then break;
17           for l:=1 to n do
18            begin
19             if l≠i then
                begin
20                otheri:=flag[l];
21                if otheri≥k then break;
                end;
22            end;
23        until otheri<k;
24      end;
25   criticalSection;
26   flag[i]:=0;
27   remainderSection;
28  end;
29 end.
 

Zmienna flag[i] przechowuje informacje o etapie wchodzenia do sekcji krytycznej i-tego procesu, etapów jest n-1, gdzie n - to liczba procesów ubiegających się o zasoby. Zmienna turn[k] przechowuje informacje o procesie o najniższym priorytecie na k-tym etapie. Jeżeli proces i-ty jest tym o najniższym priorytecie, wtedy musi on wykonać pętle for (linie od 17 do 22). Pozostałe procesy przechodzą od razu do następnego etapu dotarcia do sekcji krytycznej. W ten sposób, na każdym następnym etapie zostaje jeden proces. Każdy z tych pozostających procesów może przejść do następnego etapu, jeżeli żaden inny proces nie jest na etapie dalszym (warunek z linii 23). Po wykonaniu sekcji krytycznej, etap dotarcia procesu ustawiany jest na 0 (linia 26), co oznacza, że proces będący na najwyższym etapie, może przejść do następnego etapu, gdyż żaden inny proces nie jest bliżej sekcji krytycznej.

[edytuj] Algorytm Lamporta dla n procesów

 
01 program LAMPORTALGORITHM;
 
02 begin
03 shared
04    choosing[1..n]: 0..1;
05    num[1..n]: INTEGER;
06 local
07    testi: 0..1;
08    k, minei : INTEGER;
09    otheri, tempi: INTEGER;
 
10 while True do
11 begin
 
12    choosing[i]:=1;
13    for k:=1 to n do
14       if k≠i then
15        begin
16         tempi:=num[k] ;
17         minei:=max(minei,tempi);
18        end;
19    minei:= minei+1;
20    num[i]:= minei;
21    choosing[i]:=0;
 
22    for k:=1 to n do
23      if k≠i then
24        begin
25          repeat
26            testi:=choosing[k];
27          until testi=0;
28          repeat
29            otheri:=num[k];
30          until otheri=0 or (minei,i)<(otheri,k);
31        end;
 
32    criticalSection;
33    num[i]:=0 ;
34    otherStuff;
35 end;
36 end.
 

Algorytm ten opiera się na przydzielaniu numerów wszystkim procesom, które chcą wejść do sekcji krytycznej. Pierwszej fazie (linie 12-21) przydzielamy procesowi numer - największy dotychczas przydzielony + 1. Następnie w drugiej fazie dla każdego procesu sprawdzamy czy aby nie jest nadal w fazie pierwszej (linie 25-27) oraz czy jego numer jest większy od naszego. Przydzielane numery pełnią tutaj rolę priorytetów - im mniejszy numer tym wyższy priorytet.

Nie ma jednak sposobu, aby zapewnić, że zawsze zostanie przydzielony unikatowy numer (dwa procesy mogą na raz wykonywać pierwszą fazę i uzyskać ten sam numer. Dlatego jako dodatkowy współczynnik wykorzystuje się unikatowy id procesu - reprezentowany przez zmienną i. De facto dokonuje się tego w linii 30. (minei,i)<(otheri,k) można rozpisać jako (minei < otheri) or (minei = otheri and i < k).

Algorytm ten znany jest również pod nazwą bakery algorithm.

Co by się stało po usunięciu linii 25-27:

num[0] = num[1] = 0
Proces nr 0 (i = 0) Proces nr 1 (i = 1)
... ...
19: minei = 1, num[0] = 0
20: num[1] = 1
...
29: (k = 0) otheri = 0
30: until otheri = 0 or ... (OK)
sekcja krytyczna
20: num[0] = 1
...
29: (k = 1) otheri = 1
30: until otheri = 0 or (minei, i) < (otheri, k) // (1, 0) < (1, 1) (OK)
sekcja krytyczna


Pewnym problemem związanym z algorytmem Lamporta jest skończony zakres zmiennej mine. Przykładowe rozszerzenie zapobiegające 'przekręceniu' wygląda następująco:

 
01 program LAMPORTALGORITHMv2.0;
 
02 begin
03   shared
04     choosing[1..n]: 0..1;
05     num[1..2][1..n]: INTEGER;
06     active: 1..2;
07     bank: 1..2;
08     MAX: INTEGER := MAX_INT-n;
09   local
10     testi: 0..1;
11     k, minei : INTEGER;
12     otheri, tempi: INTEGER;
13     mybanki: 1..2;
 
14   while True do
15     begin
 
16       minei:=0;
17       choosing[i]:=1;
18       mybanki:=bank;
19       for k:=1 to n do
20         if k≠i then
21           begin
22             tempi:=num[mybanki][k];
23             minei:=max(minei,tempi);
24           end;
25       minei:= minei+1;
26       if mine>MAX then
27         bank:=(mybanki mod2)+1;
28       num[mybanki][i]:= minei;
29       choosing[i]:=0;
 
30       while mybanki<>active do;
31       for k:=1 to n do
32         if k≠i then
33           begin
34             repeat
35               testi:=choosing[k];
36             until testi=0;
37             repeat
38              otheri:=num[mybanki][k];
39             until otheri=0 or (minei,i)<(otheri,k);
40           end;
 
41       criticalSection;
42       num[mybanki][i]:=0 ;
43       remainderSection;
44     end;
45  end.
 
 
46 program SUPERVISOR;
 
47 begin
48   shared
49     choosing[1..n]: 0..1;
50     num[1..2][1..n]: INTEGER;
51     active: 1..2;
52     bank: 1..2;
53     MAX: INTEGER := MAX_INT-n;
54   local
55     test: 0..1;
56     k : INTEGER;
57     other: INTEGER;
 
58   while True do
59     begin
 
60       while bank=active do;
61       for k:=1 to n do
62         begin
63           repeat
64             test:=choosing[k];
65           until test=0;
66           repeat
67             other:=num[active][k];
68           until other=0;
69         end;
70       active:=bank;
71     end;
72 end.
 

Rozwiązanie to polega na wykorzystywaniu na zmianę dwóch banków numerów. Gdy jakiś proces stwierdzi, że nadany mu numer jest odpowiednio duży (większy od wartości MAX), przełącza bank na przeciwny, w wyniku czego kolejnym procesom nadawane są numery od zera. Procesy te jednak po otrzymaniu numeru muszą czekać, aż ich bank stanie się aktywnym (linia 30). Przełączeniem zajmuje się wyróżniony proces nadzorcy, który nic nie robi, dopóki aktywnym bankiem jest aktualnie używany (linia 60), a w przeciwnym wypadku czeka do momentu, aż aktywny bank zostanie wyzerowany (wszystkie procesy posiadające numery z tego banku opuszczą sekcję krytyczną), po czym ustawia wartość zmiennej active na ten bank, z którego są aktualnie przydzielane numery (linia 70).

Rozwiązanie to działa poprawnie dla liczby procesów mniejszej od największej wartości typu Integer podzielonej przez dwa (wartość MAX musi być większa od liczby procesów).

Inne rozwiazanie 2008 (wyklady) zapobiegajace przepelnieniu licznika

 
1 program LAMPORTALGORITHM;
 
02 begin
03 shared
04    choosing[1..n]: 0..1;
05    num[1..n]: INTEGER;
//------------------------
updating := 0; // okresla czy nastepuje "przesuniecie kolejki"
//------------------------
06 local
07    testi: 0..1;
08    k, minei : INTEGER;
09    otheri, tempi: INTEGER;
 
10 while True do
11 begin
 //------------------------
while updating = 1 do; // jesli następuje odświeżanie kolejki to się
                       // wstrzymujemy z szukaniem numerka
//------------------------
12    choosing[i]:=1;
13    for k:=1 to n do
14       if k≠i then
15        begin
16         tempi:=num[k];
17         minei:=max(minei,tempi);
18        end;
19    minei:= minei+1;
20    num[i]:= minei;
21    choosing[i]:=0;
 
22    for k:=1 to n do
23      if k≠i then
24        begin
25          repeat
26            testi:=choosing[k];
27          until testi=0;
28          repeat
29            otheri:=num[k];
30          until otheri=0 or (minei,i)<(otheri,k);
31        end;
//------------------------
updating := 1;  // zaznaczamy, że zaczyna się przesuwanie numerków w
                // kolejce
for k:= 1 to n do
    while choosing[k] do; // jeśli ktoś jeszcze wybiera numerek, to
                          // wstrzymujemy się z przesuwaniem kolejki, aż sobie nie wybierze
                          // w tym miejscu nikt nie zacznie wybierania numerka (numerku?)
for k:= 1 to n do
        if num[k] != 1
             num[k] := num[k] -1; // zmniejszamy wszystkim numerek o 1,
                                  // chyba że oznaczaloby to zmianę wartości na 0
updating := 0;                    // kończymy przesuwanie kolejki
//------------------------
 
 
32    criticalSection;
33    num[i]:=0 ;
34    otherStuff;
35 end;
36 end.
 
Osobiste
Przestrzenie nazw
Warianty
Działania
Nawigacja
Narzędzia