Sunteți pe pagina 1din 12

6.7.

Terminarea programelor distribuite

Tehnica jetonului poate fi folosită cu succes la detectarea terminării proceselor într-


un program paralel. Un program paralel se termină dacă toate procesele sale se
termină. Există însă situaţii în care procesele ce compun un program paralel conţin
cicluri infinite. Definiţia anterioară nu este aplicabilă în astfel de situaţii, fiind
necesară adoptarea unei variante mai cuprinzătoare. Astfel, considerăm că un program
este terminat dacă fiecare proces este blocat sau terminat şi nu există nici o cerere de
intrare/ieşire în curs de execuţie. Detecţia terminării se poate face simplu în cazul cînd
toate procesele se execută pe acelaşi procesor: coada proceselor pregătite pentru
execuţie este goală.

În cazul unui algoritm distribuit detectarea terminării este mai dificilă, deoarece nu
există nici o cale de a "capta" starea instantanee a tuturor proceselor. Deci, chiar dacă,
la un moment dat, un proces termină prelucrarea, el poate fi reactivat ulterior de un
mesaj primit de la un alt proces al programului. Din acest motiv, stabilirea terminării
unui program necesită comunicarea unor mesaje de control suplimentare între
procesele sale.

Terminarea proceselor organizate într-un inel (tehnica jetoanelor)

Pentru început vom analiza o variantă mai simplă, în care procesele comunică între
ele prin canale formînd un inel: P(1) trimite mesaje doar lui P(2), P(2) doar lui P(3) şi
aşa mai departe. În general, procesul P(i) primeşte mesaje pe canalul ch[i] şi trimite
mesaje pe canalul ch[(i mod n) + 1].

Condiţia de terminare a colecţiei de procese este ca fiecare proces să fie liber şi pe


canale să nu existe mesaje în tranzit. Un proces se consideră liber dacă a terminat
execuţia sau dacă se află în aşteptarea unui mesaj (receive). Un mesaj este în tranzit
dacă a fost transmis dar nu a fost încă recepţionat.

Pentru a detecta terminarea folosim un jeton pe care procesele şi-l trimit pe aceleaşi
canale pe care le folosesc pentru mesajele uzuale. Cînd procesul care deţine jetonul la
un moment dat se termină, el trimite jetonul mai departe, procesului următor. Fiecare
dintre procesele următoare participă la transmiterea jetonului în continuare (convenim
ca un proces să participe la transmiterea jetonului în algoritmul de stabilire a
terminării întregului program, chiar dacă el nu mai participă la calcule). Ca urmare, la

88
un moment dat, jetonul va ajunge înapoi la iniţiatorul acţiunii de terminare. Poate
acesta decide că programul s-a terminat?

La prima vedere, da! Dar, nu trebuie uitat că iniţiatorul poate primi mesaje obişnuite
fiind reactivat, înainte de primirea jetonului înapoi. Ca urmare, pentru a decide
terminarea, este nevoie de o condiţie suplimentară: după generarea jetonului de
terminare, iniţiatorul să nu mai primească alte mesaje. Pentru a putea face această
verificare, marcăm starea procesului, la transmiterea jetonului, cu albastru şi îi
schimbăm culoarea în roşu dacă primeşte un mesaj obişnuit şi face calcule. Dacă
procesul primeşte jetonul înapoi şi culoarea sa a rămas albastru atunci el poate decide
terminarea programului paralel.

Aceste elemente sunt cuprinse în regulile ce urmează, în care, pentru exprimarea


predicatului RING, s-a asociat jetonului o variabilă auxiliară, care numără canalele
goale. Se presupune că iniţiatorul terminării este procesul P(1).

{RING: P(1) este albastru => P(1),...,P(jeton+1) sunt albastre şi ch[2],...,ch[(jeton


mod n) + 1] sunt goale}

acţiunile lui P(1) cînd devine liber prima dată:


culoare[1] := albastru;
jeton := 0;
send ch[2](jeton);

acţiunile lui P(i:1..n) la recepţia unui mesaj obişnuit:


culoare[i] := rosu;

acţiunile lui P(i:2..n) la primirea unui jeton:


culoare[i] := albastru;
jeton := jeton +1;
send ch[(i mod n) + 1](jeton);

acţiunile lui P(1) la recepţia jetonului:


if culoare[1] = albastru -> anunţă terminarea şi stop fi
culoare[1] := albastru;
jeton := 0;
send ch[2](jeton);

Terminarea în cazul general (tehnica jetoanelor)

89
Algoritmul se complică dacă topologia reţelei de procesoare este diferită de un inel.
Deoarece canalele utilizate de procesele unui program paralel sunt entităţi globale, o
topologie de graf complet, în care fiecare proces poate comunica mesaje tuturor
celorlalte, nu este exclusă.

Cheia algoritmului pentru topologia inel este că jetonul parcurge toate legăturile
posibile între procese, golind canalele de comunicare de mesajele uzuale. Putem
extinde această metodă la noua topologie, cerînd ca jetonul să parcurgă fiecare arc al
grafului. Aceasta înseamnă că fiecare proces este vizitat de mai multe ori. Dacă la
fiecare vizitare procesul se menţine liber, putem decide terminarea întregii colecţii de
procese.

Fie c un ciclu care include toate arcele grafului şi fie nc lungimea lui. Procesele
păstrează o urmă a acestui ciclu, astfel că la fiecare vizitare ele aleg următoarea
legătură pe care transmit jetonul. Ca şi mai înainte, jetonul poartă numărul de legături
găsite libere. Spre deosebire de cazul precedent, nu este sigur că dacă la capătul
ciclului jetonul găseşte procesul iniţiator la culoarea de start, celelalte procese nu au
schimbat mesaje între ele. Ca urmare, de data aceasta este nevoie de o regulă diferită
de pasare a jetonului şi de o condiţie diferită de decidere a terminării.

Jetonul porneşte de la orice proces şi are iniţial valoarea 0. Cînd procesul devine liber
pentru prima dată, se colorează în albastru şi transmite jetonul prin prima legătură a
ciclului c. La recepţia unui jeton, un proces face următoarele acţiuni:
{GRAF: jetonul are valoarea T => ultimele T canale vizitate de jeton erau goale şi
toate P(i) care au transmis jetonul erau albastre cînd au transmis jetonul}

acţiunile lui P(i:1..n) la primirea unui mesaj uzual:


culoare[i] := rosu;

acţiunile lui P(i:1..n) la recepţia jetonului:


if jeton = nc -> anunţă terminarea şi stop fi;
if culoare[i] = rosu -> culoare[i] := albastru;
jeton := 0
[] culoare[i] = albastru -> jeton := jeton +1
fi;
setează j la canalul următor din ciclul c;
send ch[j](jeton);

Regulile enunţate asigură că GRAF este un invariant global. Îndată ce jetonul atinge
valoarea nc, se poate decide că toate procesele sunt libere şi toate canalele sunt goale.
90
În fapt, jetonul parcurge ciclul de două ori pentru a decide terminarea: o dată pentru a
pune procesele la culoarea albastru şi a doua oară pentru a verifica păstrarea culorii de
la ultima modificare.

Exerciţiu
Extindeţi rezolvarea detecţiei terminării prin jetoane, de la cazul unui graf complet la
cel al unui graf oarecare.

91
Detecţia terminării folosind confirmările mesajelor

Reamintim că terminarea unui program distribuit este detectată atunci cînd toate
procesele sunt libere şi toate canalele sunt goale. În soluţiile prezentate, se foloseşte
un singur jeton, care culege informaţii despre starea tuturor canalelor folosite de
program. Funcţia de determinare a stării canalelor se poate împărţi între procesele
programului: fiecare proces verifică acele canale pe care a transmis mesaje altor
procese şi care constituie, deci, canalele sale de ieşire.

Pentru a determina starea canalelor se folosesc două variante. Prima este cea a
confirmărilor: dacă pentru fiecare mesaj transmis pe un canal, procesul primeşte de
la receptor un semnal de confirmare, atunci canalul respectiv este gol. A doua variantă
foloseşte mesaje de marcaj, transmise pe aceleaşi canale cu mesajele de date. Un
proces transmite un mesaj de marcaj, după ultimul mesaj de date transmis pe o
legătură. Doar marcajele sunt confirmate. Dacă procesul nu transmite alte mesaje
după marcaj şi primeşte confirmarea acestuia, atunci canalul respectiv este gol.

În continuare sunt prezentate două soluţii ce folosesc aceste variante pentru detecţia
terminării unei reţele de procese, ale căror legături de comunicare a datelor alcătuiesc
un graf oarecare. Ambele presupun existenţa unui proces sursa, care nu are legături de
intrare şi de la care orice alt proces este accesibil pe o cale oarecare din graf (vezi
figura 6.4), în care legăturile arborelui de acoperire sunt marcate cu linie dublă.

+---+ +---+ +---+


| 1 |============>| 2 |===============>| 4 |
+---+ +---+ +---+
| || ^ |
| || | |
| \/ | |
| +---+ |
+-------------->| 3 |<-----------------+
+---+
Figura 6.4

Prelucrarea începe cînd sursa trimite un mesaj pe fiecare legătură de ieşire. Cînd un
proces primeşte primul mesaj, el poate începe prelucararea proprie şi poate transmite
şi recepţiona alte mesaje pe legăturile sale. Procesul se termină atunci cînd funcţia sa
este realizată complet, dar poate reîncepe prelucrarea dacă primeşte alte mesaje.

92
Algoritmul Dijkstra-Scholten se bazează pe transmiterea între procese a unor
semnale de confirmare. Schema de semnalare a terminării este separată şi
suplimentară faţă de prelucrarea propriu-zisă pe care o realizează procesul. Ea
urmăreşte informarea sursei despre terminarea colecţiei de procese. Procesele pot
recepţiona şi transmite semnalele, chiar şi atunci cînd sunt libere, deci au terminat
prelucrarea caracteristică a datelor. Semnalele se transmit, evident, în sens invers
datelor, reprezentînd confirmări ale preluării acestora de către destinatar.

Dacă procesele ar fi organizate într-o reţea cu topologie arborescentă, algoritmul de


semnalare ar fi simplu: cînd un proces frunză se termină, el anunţă părintele său. Cînd
un nod oarecare al arborelui se termină, el aşteaptă notificarea terminării de la toţi fiii
săi şi apoi îşi anunţă părintele. Notificările se propagă astfel pînă la procesul sursa,
aflat în rădăcina arborelui.

Acest algoritm poate fi extins la un graf dirijat aciclic de procese. Fiecărei legături i
se asociază un număr, reprezentînd diferenţa dintre numărul de mesaje de date
transmise şi numărul de semnale de confirmare primite pe acea legătură. Numim
deficit această diferenţă. Cînd un nod doreşte să termine, el aşteaptă primirea pe
legăturile de ieşire a datelor, a unor semnale care reduc deficitele la zero, după care
trimite pe fiecare legătură de intrare a datelor un număr de semnale egal cu deficitul.

Algoritmul Dijkstra-Scholten rezolvă problema în cazul general, în care sunt


permise cicluri în graful proceselor. Rezolvarea se face prin generarea unui arbore de
acoperire, pe parcursul transferului mesajelor de date: fiecare proces păstrează o
variabilă prim a cărei valoare este egală cu identificatorul procesului de la care s-a
recepţionat primul mesaj de date.

Rezolvarea implică trei faze:


- trimiterea semnalelor pe toate legăturile de intrare, cu excepţia celei de la prim;
- recepţia semnalelor pe toate legăturile de ieşire;
- transmiterea semnalelor spre prim.

Pentru recepţia semnalelor pe legăturile de ieşire, se poate ţine o evidenţă globală,


deoarece este important ca deficitele legăturilor de ieşire să se anuleze, neavînd
importanţă identitatea legăturilor pe care se primesc semnale. Ca urmare, o singură
variabilă nsemnale care să ţină minte suma deficitelor legăturilor de ieşire este
suficientă.

93
Algoritmul fiecărui nod este repartizat în două procese: Prelucrare(i) realizează
transformarea datelor; Nod(i) realizează gestiunea mesajelor şi a semnalelor
schimbate cu celelalte noduri. Prelucrare(i) are un canal chm[i] prin care primeşte
mesajele de date. Nod(i) are un canal chs[i] prin care primeşte semnale de la celelalte
procese, anunţuri de transmitere/recepţie de mesaje de date şi cereri de terminare de la
Prelucrare(i). Răspunsul la cererile de terminare este dat pe canalele stop[i].

chm[i] +-------------+ chm[j]


--> ]]]]]]-->-|Prelucrare[i]| -----> ]]]]]]---> Prelucrare[j]
+-------------+
| | <------- Nod[j]
+-+ +-+
stop[i]+-+ +-+ chs[i]
+-+ +-+
+-+ +------+ |
+<-|Nod[i]|<-+
+------+
Figura 6.5

Topologia (virtuală a) reţelei de procese este păstrată în tabloul in. Fiecare element al
acestuia înregistrează existenţa legăturii (cîmpul exista) şi deficitul (cîmpul deficit).

Procesul Prelucrare (i) are o funcţionare ciclică. După recepţia unui mesaj de date, el
realizează prelucrările cerute şi, eventual, transmite mesaje de date altor procese. El
însuşi poate primi mesaje de date de la celelalte procese, pe canalele de intrare.
Recepţiile şi transmiterile de mesaje sunt anunţate procesului Nod(i). La sfîrşitul
prelucrării, procesul trimite lui Nod(i) o cerere de terminare şi aşteaptă recepţia unei
confirmări pe un canal special stop[i].

Procesul Nod(i) are o funcţionare ciclică. La fiecare iteraţie, el tratează unul din
mesajele primite de la procesul de prelucrare local sau de la celelalte procese Nod.
Pentru un anunţ de recepţie de date, procesul actualizează deficitul legăturii de intrare
şi, eventual, prim. Un anunţ de transmitere determină incrementarea variabilei
nsemnale. Un semnal determină decrementarea variabilei nsemnale. În fine, o
cerere de terminare provoacă transmiterea de semnale pe legăturile de intrare. Dacă
nsemnale este nul, atunci se transmit semnale către nodul părinte din arborele de
acoperire (prim) şi se confirmă terminarea. Altfel, se transmite o infirmare a
terminării.

Algoritmul are următoarea descriere:

94
type fel = enum(semnal, transmitere, receptie, terminare);
type arc = rec (exista: bool;
deficit: int;);
chan chs[1:n](op: fel, id: int);
chan chm[1:n](id:int, date: tip_date);
chan stop[1:n](bool);

Nod(i:1..n)::
var in: array[1:n] of arc;
var nsemnale: int :=0;
var prim: int :=0;
var id: int, k: fel;

do true ->
receive chs[i](k, id);
if k = receptie ->
if prim = 0 -> prim := id fi;
in[id].deficit := in[id].deficit+1;
[] k = transmitere ->
nsemnale := nsemnale+1;
[] k = semnal ->
nsemnale := nsemnale-1;
[] k = terminare ->
fa j := 1 to n st in[j].exista and (j<>prim) ->
do in[j].deficit>0 ->
send chs[j](semnal, i);
in[j].deficit := in[i].deficit-1;
od
af;
if nsemnale<>0 -> send stop[i](false);
[] nsemnale=0 ->
if (i<>sursa) and (prim<>0) ->
do in[prim].deficit>0 ->
send chs[prim](semnal, i);
in[prim].deficit := in[prim].deficit-1;
od;
fi
send stop[i](true);
fi
od;

Prelucrare(i:1..n)::
var data: tip_date, id: int, term: bool;
initializari;
term := false;
do not term ->
95
receive chm[i](id, data);
send chs[i](receptie, id);
realizeaza prelucrare
....
send chm[j](i, data);
send chs[i](transmitere, j);
...
if decizie de terminare -> term := true; fi
od;
term :=false;
do not term ->
send chs[i](terminare, i);
receive stop[i](term);
od;
stop;

De notat că dacă nsemnale<>0, nu se aşteaptă primirea semnalelor nerezolvate şi se


transmite procesului de prelucrare un răspuns negativ. În varianta prezentată, procesul
de prelucrare reîncearcă terminarea pînă cînd aceasta reuşeşte, dar, în cazul general,
el poate primi mesaje care să-l repună în execuţie (codul trebuie modificat
corespunzător). Starea finală a actualului algoritm este cea în care procesul sursă
raportează terminarea, iar în celelalte noduri, procesele au terminat prelucrările.

Detecţia terminării folosind marcaje

În cea de a doua variantă, procesele transmit mesaje de marcaj al sfîrşitului


comunicaţiei, urmînd să primească o confirmare doar pentru aceste mesaje, nu şi
pentru cele de date. Cînd un proces decide terminarea, el aşteaptă marcaje pe toate
legăturile de intrare şi transmite un marcaj pe fiecare legătură de ieşire. Îndată ce
procesul a primit marcaje, el trimite confirmări pe legăturile de intrare
corespunzătoare, mai puţin pe prima legătură. Apoi, el aşteaptă semnale pe legăturile
de ieşire. Cînd le primeşte, transmite un semnal pe prima legătură.

Structurile utilizate pentru cele două categorii de arce au aceeaşi sintaxă. Ele conţin
câmpurile exista, activ şi marcaj. De data aceasta, fiecare arc de intrare
înregistrează dacă:
- s-a primit un mesaj (se pune activ pe true);
- s-a primit un marcaj (se pune marcaj pe true);
- s-a transmis un semnal de confirmare (se pun ambele pe false).
Similar, pentru fiecare arc de ieşire, se înregistrează dacă:
- s-a transmis un mesaj (se pune activ pe true);

96
- s-a transmis un marcaj (se pune marcaj pe true);
- s-a primit un semnal de confirmare (se pun ambele pe false).

const marcaj = mesaj-special-de-marcaj-sfirsit-date;


type fel = enum(semnal, transmitere, receptie,
receptie-marcaj, terminare);
type arc = rec (exista: bool;
activ: bool; {initial false}
marcaj: bool;); {initial false}
chan chs[1:n](op: fel, id: int);
chan chm[1:n](id:int, date: tip_date);
chan stop[1:n](bool);

Nod(i:1..n)::
var term: bool := false;
var in, out: array[1:n] of arc;
var nsemnale: int :=0; {numar semnale asteptate pe out}
var prim: int :=0;
var nmarcaje: int; {numar marcaje asteptate pe in}
var id: int, k: fel;

do true ->
receive chs[i](k, id);
if k = receptie ->
if prim = 0 -> prim := id fi;
if not in[id].activ -> in[id].activ := true;
nmarcaje := nmarcaje+1;
fi;
[] k = receptie-marcaj ->
in[id].marcaj :=true; nmarcaje := nmarcaje-1;
if id=prim -> term := true fi;
[] k = transmitere ->
if not out[id].activ -> out[id].activ := true;
nsemnale := nsemnale+1;
fi
[] k = semnal ->
out[id].activ := false; out[id].marcaj := false;
nsemnale := nsemnale-1;
[] k = terminare ->
if not term -> send stop[i](false);
[] term ->
fa j := 1 to n st j<>prim and out[j].activ and
not out[j].marcaj ->
send chm[j](i, marcaj); out[j].marcaj := true;
af;
if nmarcaje <> 0 -> send stop[i](false);
[] nmarcaje = 0 ->
97
fa j := 1 to n st in[j].exista and (j<>prim)
and in[j].activ ->
send chs[j](semnal, i);
in[j].activ := false; in[j].marcaj := false;
af
if nsemnale<>0 -> send stop[i](false);
[] nsemnale=0 ->
if (i<>sursa) and (prim<>0) ->
send chs[prim](semnal, i);
in[prim].activ := false;
in[prim].marcaj := false;
fi;
send stop[i](true);
fi
fi
fi
fi
od;

Prelucrare(i:1..n)::
var data: tip_date, id: int, term: bool;
initializari;
term := false;
do not term ->
receive chm[i](id, data);
if data=marcaj -> send chs[i](receptie-marcaj,id);
[] data<>marcaj ->
send chs[i](receptie, id);
realizeaza prelucrare
....
send chm[j](i, data);
send chs[i](transmitere, j);
...
if decizie de terminare -> term := true;
fi
od;
term :=false;
do not term ->
send chs[i](terminare, i);
receive stop[i](term);
od;
stop;

98
Termination Detection

Model

– processes can be active or idle


– only active processes send messages
– idle process can become active on
receiving an computation message
– active process can become idle at any time
– termination: all processes are idle and no computation message are in transit
– Can use global snapshot to detect termination also

Huang’s Algorithm

• One controlling agent, has weight 1 initially


• All other processes are idle initially and have weight 0
• Computation starts when controlling agent sends a computation message to a process
• An idle process becomes active on receiving a computation message
• B(DW) – computation message with weight DW. Can be sent only by the controlling agent
or an active process
• C(DW) – control message with weight DW, sent by active processes to controlling agent
when they are about to become idle
Let current weight at process = W

1. Send of B(DW):
• Find W1, W2 such that W1 > 0, W2
> 0, W1 + W2 = W
• Set W = W1 and send B(W2)

2. Receive of B(DW):
• W += DW;
• if idle, become active

3. Send of C(DW):
• send C(W) to controlling agent
• Become idle

4. Receive of C(DW):
• W += DW
• if W = 1, declare “termination”

99