Sunteți pe pagina 1din 22

51.

Modelul producătorului şi consumatorului


O schemă uzuală de comunicare este cea în care un proces (producătorul) trimite mesaje unui alt proces
(consumatorul), utilizând un tampon în memoria comună. Mesajele sunt de lungime fixă şi capacitatea tamponului este
de N mesaje.
Specificaţiile comunicaţiei sunt următoarele:
 un mesaj dat poate fi preluat doar o singură dată după ce a fost depozitat în tampon,
 un mesaj nu trebuie să poată fi pierdut; dacă tamponul conţine N mesaje nepreluate, nu pot fi depozitate aici
mesaje suplimentare,
 o operaţie “imposibilă” (depozitare într-un tampon plin sau preluare dintr-un tampon vid) blochează procesul,
care încearcă să o execute.
Condiţiile de depăşire pot fi exprimate după cum urmează, notând prin n numărul de mesaje din tampon, care nu au
fost încă preluate:
aut(depozitare) : n < N -- tamponul nu este plin
aut(preluare) : n > 0 -- tamponul nu este vid
Respectarea acestor restricţii este asigurată de un monitor tampon, utilizat după cum urmează:
proces producător proces consumator
... ...
produce(mesaj_emis); tampon.preluare(mesaj_recepţionat);
tampon.depozitare(mesaj_emis); consumator(mesaj_recepţionat);
Monitorul tampon poate fi elaborat, transcriind în mod direct autorizările de depăşire:
tampon : monitor;
var n : 0..N;
non_plin, non_vid : condiţie;
<declaraţii ale procedurilor depozitare şi preluare>
procedura depozitare(m:mesaj);
begin
if n=N then
non_plin.aşteptare
endif;
n:=n+1;
introducere(m);
non_vid.semnalizare
end
procedura preluare(var m:mesaj);
begin
if n=0 then
non_vid.aşteptare
endif;
preluare(m)
; n:=n-1;
non_plin.semnalizare
end;
begin -- iniţializare
n:=0;
end
end tampon
Procedurile introducere(m) şi preluare(m) definesc politica de gestionare a tamponului şi reprezentarea internă a
mesajelor. Un caz frecvent este acela în care mesajele sunt reprezentate de elementele succesive ale unui tablou,
administrat în mod circular. În acest caz mesajele sunt preluate în ordinea depozitării. Procedurile de gestionare a
tamponului se vor scrie astfel:
type mesaj : <descrierea formatului
mesajelor> ptr : 0..N-1;
var fa : array[ptr] of
mesaj; top, coadă: ptr;
procedura intrare(m:mesaj);
begin
fa[coadă]:=m;
coadă:=coadă+1 mod
N end;
procedura ieşire(var m:mesaj);
begin
m:= fa[top];
top:=top+1 mod
N end;
<iniţializarea se va completa cu top:=0; coadă:=0;>
Această schemă poate fi extinsă pentru mai mulţi producători şi consumatori. Drept efect, procedurile monitorului
asigură accesarea exclusivă la mesaje între producători şi consumători. Totuşi, în cazul a mai mulţi consumatori schema
nu permite direcţionarea unui mesaj către un consumator anumit: putem doar garanta, că un mesaj va fi preluat de un
consumator (şi numai de unul singur) fără a specifica concret de care.

52. Primitive de comunicare


Schimbul de mesaje între procese, în afară de funcţia de transmitere a informaţiei, poate fi utilizat şi pentru
ordonarea evenimentelor în cadrul unor procese distincte, deoarece emiterea unui mesaj precede întotdeauna recepţia sa.
Operaţiile de schimb de mesaje pot fi definite ca nişte mecanisme primitive şi să le utilizăm pentru sincronizarea
proceselor.
Primitivele de bază în comunicarea prin mesaje sunt:
emitere(mesaj,destinaţie)
recepţie(mesaj,origine)
Specificările acestor primitive trebuie să precizeze:
 natura şi forma mesajelor,
 modul de adresare a proceselor emiţătoare şi destinatare,
 modul de sincronizare a acestor procese,
 tratarea erorilor.
1) Natura mesajelor
În conformitate cu nivelul de exprimare la care este definit mecanismul de comunicare, mesajele pot fi specificate
de un tip, analogic obiectelor unui limbaj sau prin lungimea fizică a lor. Lungimea poate fi constantă sau variabilă. Mai
frecvent sunt utilizate mesajele de lungime constantă, care pot fi create mai simplu, mesajele de lungime variabilă vor fi
transmise prin referinţă, pasând adresa fizică sau identificatorul informaţiei transmise.
2) Modul de adresare
Procesele, care fac schimb de mesaje, se pot desemna reciproc prin numele lor (desemnare directă) sau pot utiliza
numele unui obiect intermediar ori cutie poştală (desemnare indirectă). Aceste nume sunt folosite ca parametri origine
şi destinaţie. Schema a doua facilitează modificarea dinamică a interconexiunilor proceselor sau chiar componentele
unei mulţimi de procese, care comunică.
3) Moduri de sincronizare
Pentru primitivele de comunicare pot fi specificate mai multe moduri de sincronizare. În caz general, operaţia de
recepţie blochează, în absenţa mesajului, receptorul. Unele sisteme pun la dispoziţie primitive care dau posibilitatea să
se determine dacă o cutie poştală este vidă, ceea ce permite evitarea blocării. Pentru emitere sunt utilizate două moduri
de sincronizare:
 Schema producător-consumator, în care cutia poştală este realizată printr-o zonă tampon. Emiterea nu este
blocantă, cu excepţia cazului în care tamponul este plin. Folosirea tampoanelor de lungime variabilă cu alocarea
dinamică a locaţiunilor reduce probabilitatea blocării emiţătorului.
 Schema rendez-vous, în care emiţătorul este blocat până la preluarea mesajului de către receptor. Această
schemă poate fi considerată caz limită a precedentei cu lungimea nulă a tamponului.
În fine, atunci când un proces este asociat în recepţie la mai multe porţi, poate fi definit un mod de sincronizare, zis
aşteptare multiplă, în care sosirea unui mesaj la o oarecare din aceste porţi deblochează receptorul.

4) Tratarea erorilor
Scopul tratării erorilor este de a evita blocările infinite ale proceselor, care se pot produce în diverse circumstanţe:
 Emiterea unui mesaj cu o destinaţie (proces sau poartă) inexistentă. În acest caz primitiva nu este blocantă;
eroarea este tratată prin emiterea unui cod de eroare sau printr-o deviere.
 Distrugerea unui proces de la care alte procese aşteaptă un mesaj sau un răspuns: procesele în aşteptare sunt
blocate şi recepţionează un cod de eroare.
Ultima situaţie nu este totdeauna detectabilă. O tehnică uzuală constă în stabilirea unui interval de timp maxim de
aşteptare a unui mesaj şi deblocarea procesului care aşteaptă la expirarea acestui interval (v. 3.4.5).
Vom ilustra prin câteva exemple reprezentative utilizarea acestor mecanisme de comunicare.
--Daca o sa trebuiasca de dat exemple.
Exemplul 3.12. Sistemul de operare Thoth [8]. În acest sistem comunicarea foloseşte desemnarea directă şi sincronizarea prin rendez-vous. Mesajele
sunt de lungime constantă. Sunt utilizate patru primitive:
id:=send(message, id_dest)
emite procesului id_dest un mesaj; blochează emiţătorul până la primirea unui răspuns, transmis în message. Această primitivă
indică identitatea procesului care a transmis răspunsul (sau nil, dacă destinatarul nu există).
id:=receive(message, id_orig)
recepţionează un mesaj; procesul origine poate să nu fie specificat. Valoarea transmisă este identitatea emiţătorului.
reply(message, id_orig, id_dest)
trimite un răspuns destinatarului specificat (care trebuie să-l aştepte); nu este blocantă; fără consecinţe, dacă răspunsul nu era
aşteptat.
forward(message, id_orig, id_dest)
această operaţie non blocantă este utilizată de un proces după recepţionarea unui mesaj trimis de către id_orig, pentru ca să impună
mesajul să ajungă la id_dest, care are acum obligaţia de a răspunde lui id_orig. ◄
Exemplul 3.13. Sistemul de operare Unix [9]. În sistemul Unix comunicarea între procese utilizează tampoane, numite pipes (tuburi), administrate
conform schemei producător-consumator. Mesajele transmise sunt caractere. Un pipe (tub) leagă un emiţător şi un receptor,
conexiunea fiind stabilită dinamic. ◄
Exemplul 3.14. Rendez-vous în limbajul de programare Ada [10, 11]. Limbajul Ada permite definirea proceselor. Forma sintactică a comunicărilor
între procese este apelarea procedurii, însă transmiterea parametrilor şi a rezultatelor are loc conform principiului de transmitere a
mesajelor cu rendez-vous. Recepţionarea poate fi condiţionată (un apel este acceptat doar dacă o condiţie specificată este
satisfăcută) şi există posibilitatea de aşteptare multiplă. ◄

53. Aplicaţii : relaţia client-server


O aplicaţie curentă a comunicărilor între procese este relaţia client-server. Un proces server are în şarjă
îndeplinirea unor servicii (executarea unui program predefinit) proceselor client. Pentru aceasta poate fi utilizată
următoarea schemă:
procesul server procesul client
ciclu poartă_serviciu.emitere(cerere)
poartă_serviciu.recepţionare(cerere)
<executare serviciu> ...
[poartă_client.emitere(rezultat)] ...
endciclu [poartă_client.recepţionarere(rezultat)]
Secvenţele din parantezele pătrate sunt facultative.
Procesul server este asociat unei porţi, unde clienţii îşi depun cererile, trimiţând cereri; el este blocat atâta timp cât nu
există cereri de servicii în aşteptare.
Serviciul cerut poate conţine trimiterea la client a rezultatului. În acest caz clientul trebuie să trimită serverului în
cererea sa numărul unei porţi la care el se va bloca în aşteptarea rezultatului.
Fără a modifica schema de mai sus putem introduce mai multe procese server echivalente, oricare dintre ele fiind în
stare să satisfacă o cerere a unui serviciu. Aceste servere în recepţie vor fi asociate la una şi aceeaşi cutie poştală.
Modelul din 3.4.2.1 (alocarea resurselor banalizate) şi modelul client-server de mai sus sunt reprezentative pentru două
scheme de obţinere a unui serviciu cu ajutorul proceselor într-un sistem de operare: apelarea de procedură într-un monitor sau
activarea uni proces server ciclic prin emiterea mesajelor. Alegerea între aceste două scheme este dictată de considerente de
eficacitate (schema serverului este preferată, atunci când există paralelism real între client şi server) sau de uniformitate a
structurii.

54. Administrarea intrărilor-ieşirilor


Vom arăta cum poate fi integrată administrarea intrărilor-ieşirilor în mecanismul monitoarelor. Prezentăm mai întâi
gestionarea unui periferic izolat, apoi, ca exemplu de aplicaţie, principiul unei gestionări tamponate a intrărilor-ieşirilor.
55. Administrarea unui periferic
Fiecărui periferic îi este asociat un monitor procedurile externe ale căruia permit executarea intrărilor-ieşirilor la
acest periferic. Acest monitor are următoarea formă generală (pentru un sistem mono-utilizator):
perif: monitor;
var ..., sfr_schimb_i,...: condiţie;
<declaraţiile variabilelor de stare ale perifericului>
...
procedura schimb_i(<parametri>);
begin
<mascarea întreruperilor>;
if starea ≠ preg then
<tratare eroare(perifericul nu este gata)>
endif;
lansare_transfer_i(parametri);
sfr_schimb_i.aşteptare; -- întrerupere demascată
if starea ≠ ok then -- în timpul aşteptării
<tratare eroare(incident de transfer)>
endif;
<demascare întreruperi>
end;
...
begin
<iniţializare>
end
end
perif

Procedura lansare_transfer_i pregăteşte programul pentru schimbul cerut (construirea programului canalului sau al ADM -
Acces Direct la Memorie, ţinând cont de parametrii schimbului) şi lansează execuţia sa (instrucţiunea SIO – Start Input
Output). Procesele apelante aşteaptă sfârşitul transferului datorită condiţiei sfr_schimb_i. Sosirea unei întreruperi, care
marchează sfârşitul schimbului de tip i provoacă în mod automat executarea următoarei secvenţe:

if sfr_schimb_i.vid then <tratarea eroare întrerupere care nu este aşteptată>


else sfr_schimb_i.semnalizare
endif
Pentru un proces care execută o intrare-ieşire apelând o procedură de schimb a acestui monitor, totul se petrece ca şi
cum schimbul este sincron: la returul din procedură, informaţia a fost efectiv transferată (sau o eroare a fost
detectată şi semnalizată). Mecanismul de blocare evită aşteptarea activă şi procesorul poate fi utilizat în timpul
transferului de un alt proces

56. Buferizarea imprimării


Putem elabora acum programul administrării tamponate a imprimării, descrise în 3.1. Avem nevoie de trei tampoane
tm1 şi tm2 de capacitate N1 şi N2 în memoria centrală şi unul pe disc, td, de lungime Ndisc. Pentru simplitate
presupunem, că transferurile se fac cu blocuri constante egale cu o linie. Fiecare tampon este comandat de un monitor
cu aceeaşi structură care are rolul de a asigura excluderea mutuală şi sincronizarea condiţiilor tampon plin şi tampon
vid.
Aceste tampoane, numite tampon1, tampon2 şi tampon_disc au aceeaşi structură cu tamponul descris în 3.4.3,
înlocuind N cu N1, N2 şi Ndisc, respectiv. Definind tm1, tm2 şi td ca tablouri de elemente de lungimea unei linii şi
pointerii top şi coadă locali fiecărui monitor, procedurile de depozitare şi preluare pot fi:
<pentru tamponul 1> <pentru tamponul 2>
procedura intrare(l:linie); procedura
intrare(l:linie); tm1[coadă] := l; tm2[coadă] := l;
coadă := coadă+1 mod N1 coadă := coadă+1 mod N2
procedura ieşire(var l:linie); procedura ieşire(var
l:linie); l := tm1[top]; l := tm2[top];
top := top+1 mod N1 top := top+1 mod N2
În monitorul tampon_disc operaţiile de depozitare şi preluare sunt intrări-ieşiri, care utilizează monitorul de
gestionare a discului:
procedura intrare(l:linie);
disc.scriere(l,td[coadă]);
coadă := coadă+1 mod
Ndisc
procedura ieşire(var
l:linie);
disc.scriere(l,td[top]);
top := top+1 mod Ndisc

Programul de intrare-ieşire este realizat prin cooperarea a patru procese programul cărora este prezentat schematic
mai jos (trei procese ale sistemului de operare şi procesul utilizator). Pentru a simplifica expunerea au fost omise
secvenţele de tratare a erorilor şi am admis, că sistemul funcţionează în regim permanent fără limitarea numărului de
linii la imprimare. Programele folosesc trei monitoare de gestionare a perifericelor (tampon1, tampon2 şi tampon_disc)
şi două monitoare de gestionare a perifericelor (impr şi disc), construite în baza modelului perif, prezentat în 3.4.4.1
proces imprimare linie proces scriere_disc proces citire_disc
ciclu ciclu ciclu
tampon2.preluare(l); tampon1.preluare(l); tampon_disc.citire(l);
impr.scriere(l); tampon_disc.scriere(l); tampon2.depozitare(l);
endciclu endciclu endciclu
Imprimarea unei linii este cerută de procedura:
procedura
scriere_linie(l:linie);
tampon1.depozitare(l)
Putem constata, că programele de mai sus sunt mult mai simple decât cele care folosesc direct întreruperile.
Structura modulară, introdusă de monitoare permite separarea totală a gestionării tampoanelor de cea a perifericelor.
Schimbarea capacităţii unui tampon modifică doar monitorul care comandă acest tampon; înlocuirea unui periferic cu
un altul implică rescrierea doar a monitorului, care comandă acest periferic.
57. Sincronizare temporală
Sincronizarea temporală face ca timpul să intervină nu numai ca mijloc de ordonare a evenimentelor, dar şi ca
măsură de durată absolută. Acest mod de sincronizare este utilizat în aplicaţiile de timp real, care conţin interacţiuni cu
organe externe (comanda proceselor industriale, de exemplu). Sincronizarea temporală solicită folosirea unui ceas,
realizat prin intermediul unui oscilator cu quartz, care emite impulsuri la intervale regulate. Aceste impulsuri pot fi
utilizate pentru a declanşa o întrerupere la fiecare impuls sau pentru a decrementa în mod automat conţinutul unui
registru contor, o întrerupere este declanşată atunci când conţinutul acestui registru atinge valoare 0.
Vom utiliza a doua metodă de funcţionare. Unitatea de timp folosită este perioada ceasului.
Pentru rezolvarea problemelor principale de sincronizare temporală poate fi folosită primitiva suspendare(t), care
are ca efect blocarea procesului apelant pentru o durată (fizică) egală cu t unităţi de timp. Prezentăm principiul de
realizare a acestei primitive cu ajutorul unui ceas.*
Pentru rezolvarea acestei probleme vom fi nevoiţi să întreţinem:
 valoarea absolută a timpului (ora absolută), care măsoară la orice moment timpul trecut de la o instanţă
iniţială,
 un registru, adică o listă a proceselor care aşteaptă deblocarea, ordonat conform timpului absolut de deblocare.
Toate procesele, care apelează primitiva suspendare(t) sunt inserate în registru în poziţia, care corespunde orei sale
absolute de deblocare.
Numim ora_de_bază ora absolută a ultimei înnoiri a ceasului, adică a ultimei iniţializări a contorului; fie t_aşt
ultima valoare încărcată. Ora absolută exactă este dată la fiecare moment de timp de relaţia
ora_exactă = ora_de_bază + t_aşt - contor
(t_aşt - contor este timpul care s-a scurs după ultima înnoire a contorului). De la o întrerupere de ceas (la trecerea
contorului prin 0), după ultima înnoire s-a scurs un timp egal cu t_aşt; ora_de_bază poate, deci, fi iniţializată conform
relaţiei
ora_de_bază := ora_de_bază + t_aşt
Variabila ora_de_bază, odată iniţializată, va fi corect întreţinută cu condiţia ca registrul să nu fie nicicând vid; în
caz general această condiţie va fi asigurată introducând un proces, numit paznic:
procesul paznic
ciclu
suspendare(tmax
) endciclu
în care tmax este un interval foarte mare de timp, paznicul rămânând pentru toată perioada de lucru în coada registrului.
Mecanismele descrise sunt realizate într-un monitor numit ceas, care are două intrări: procedura suspendare (apelată
prin ceas.suspendare(t)) şi procedura tratare_întrerupere, pentru tratarea întreruperii de ceas (trecerea contorului prin
zero). Registrul este realizat cu ajutorul unui fir de aşteptare, care conţine descriptorii proceselor. Un descriptor este
format din numele procesului şi timpul absolut de deblocare; firul de aşteptare este ordonat în ordinea creşterii timpului
deblocării.
Presupunem, că întreruperea de ceas activează un proces, unica activitate a căruia constă în apelarea procedurii
ceas.tratare_întrerupere.
ceas: monitor;
type descriptor: struct
i :
proces; ora_debl :
integer end;
var contor, t_aşt;
ora_de_bază, ora_exactă: integer
deblocare : array[proces] of condiţie;

*
Problema ar fi uşor de rezolvat, dacă fiecare proces ar dispune de un ceas propriu: ar fi suficient să se introducă în contor valoarea t şi să se aştepte
întreruperea la trecerea prin zero. Realizarea primitivei suspendare cu ajutorul unui ceas unic comun presupune ataşarea fiecărui proces a unui ceas
virtual.
<declaraţiile firului de aşteptare şi procedurilor sale de acces>
procedura suspendare(durata:integer);
var proc: descriptor;
begin
ora_exactă:=ora_de_bază+t_aşt-contor;
proc.ora_deblocării:=ora_exactă+durata -- ora absolută a deblocării
proc.i := p; -- p este procesul apelant
intrare(proc,fir) -- ordonare de ora_deblocării
if proc=primul(fir) then
t_aşt:=contor:=durata;
ora_de_bază:=ora_exactă;
endif;
deblocare[p].aşteptare
end;
procedura tratare_întrerupere; -- contor în zero
var proc:
descriptor;
delta_t:integer;
begin
ieşire(proc,fir); -- primul din firul de aşteptare
ora_de_bază := ora_exactă+t_aşt;
if vid(fir) then
delta_t := tmax
else
delta_t := primul(fir).ora_deblocării – ora_de_bază;
endif;
t_aşt := contor := delta_t; -- deblocarea viitoare
deblocare[proc.i].semnalizare
end;
begin
ora_de_bază :=
<ora_iniţială>; contor :=
t_aşt := tmax;
intrare(paznic,fir)
end
end
ceas
Metoda de mai sus (întreţinerea unui registru şi a orei absolute) este utilizată pentru realizarea programelor de
simulare discretă. Un atare program realizează un model în care diferite procese, executate în regim pseudo-paralel,
reprezintă activităţi care se derulează în paralelism real. Timpul utilizat este un timp simulat, adică o variabilă, care
reprezintă trecerea timpului fizic, respectând proporţionalitatea intervalelor timpului simulat cu intervalele
corespunzătoare ale timpului fizic.

58. Gestionarea dinamică a proceselor


Doar în sistemele cele mai simple procesele sunt în număr constant şi create odată pentru totdeauna la iniţializarea
sistemului. În sistemele concurente, mai ales în cele interactive, procesele sunt comandate dinamic. Astfel, în Multics,
un proces nou este creat odată cu admiterea unui nou utilizator; în Unix, la executarea fiecărei comenzi. Primitivele de
creare şi distrugere a proceselor pot fi puse în şarja sistemului de operare sau la dispoziţia utilizatorilor. Crearea unui
proces presupune alocarea resurselor şi iniţializarea contextului, elementele căruia au fost specificate în 3.2.1.
Distrugerea unui proces eliberează toate resursele care i-au fost alocate.
Primele primitive, propuse pentru gestionarea dinamică a proceselor, au fost fork şi join. Istoric şi cronologic, aceste
operaţii au fost introduse pentru organizarea executării paralele a programelor pe un sistem multiprocesoral, noţiunea de
proces nefiind încă clară. Vom descrie câteva variante ale acestor primitive.
Fie P o procedură. Instrucţiunea
id := fork(p),
executată de un proces p (părinte), crează un proces nou q (fiul), care va fi executat paralel cu p. Primitiva fork prezintă
ca rezultat identificatorul lui q (sau nil, dacă crearea este imposibilă). Contextul iniţial al lui q este o copie a lui p, mai
puţin contorul ordinal, care este fixat la prima instrucţiune a lui p. Procesul fiu se termină cu o primitivă, numită exit sau
quit, care provoacă dispariţia sa. După ce fork crează un proces fiu q, primitiva join q permite procesului părinte să
fixeze un punct de rendez-vous cu acest fiu. Executarea lui join q blochează procesul părinte până când q nu va executa
exit. Primitivele fork şi join au avantajele şi dezavantajele instrucţiunii go to din programarea secvenţială.
Exemplul 3.15. În sistemul de operare Unix crearea unui proces poate fi realizată de către interpretorul limbajului de comandă (shell) sau cu ajutorul
instrucţiunii fork() de un program. Ultima situaţie este prezentată schematic în fig.3.4.

procesul 1 procesul 2

copie

date date

stivă stivă

procesul fiu
procesul
părinte Fig.4.4. Crearea proceselor cu ajutorul instrucţiunii fork

Efectul instrucţiunii fork():


• duplicarea procesului părinte;
• returnarea valorii pid (numărului procesului fiu) în procesul părinte;
• returnarea valorii 0 în procesul fiu:
procesul părinte procesul fiu

if (fork() == 0) if (fork() == 0)
codul procesului fiu codul procesului fiu
else else
codul procesului părinte codul procesului părinte

returnarea pid al procesului fiu (≠ 0) returnare 0


Altfel spus, în Unix primitiva fork (fără parametri) creează un proces al cărui spaţiu de lucru este o copie a spaţiului de lucru a
creatorului, inclusiv şi contorul ordinal. Diferenţa poate fi determinată consultând valoarea returnată de primitivă (0 pentru fiu;
identificatorul fiului sau nil pentru părinte). O primitivă wait permite părintelui să aştepte terminarea execuţiei unuia dintre
programele fiu (fără a putea alege care anume, dacă există mai multe). Un proces termină execuţia sa cu primitiva exit. Primitiva
exec(p) permite unui proces să schimbe contextul, apelând o procedură specificată de p.
La lansarea Unix-ului sunt create două procese: procesul numit Swaper, care administrează memoria, cu pid=0 şi procesul Init cu
pid=1, care creează toate celelalte
procese. Ilustrăm folosirea primitivelor
fork şi exec:
...
id := fork();
if id = 0 then -- eu sunt fiul
exec(p) -- programul fiului
else -- eu sunt părintele
if id = -1 then -- nil : creare imposibilă
<tratare eroare>
else
<programul părintelui>
endif
endif
Primitiva wait este utilizată după cum urmează:
id := wait(cod) -- blocare până la terminarea programului unuia dintre fii
... -- id = numărul programului fiu terminat, cod = cauza terminării ◄
59. Sincronizarea în Windows
Platforma pe 32 de biţi pune la dispoziţia programatorului instrumente evoluate pentru multiprogramare, atât la
nivelul unei mulţimi de lucrări, cât şi a unei lucrări singulare. Poate să apară întrebarea CÂND să fie utilizată
multiprogramarea în cadrul unei singure aplicaţii. Răspunsul este foarte simplu: atunci când dorim ca mai multe
fragmente de cod să fie executate simultan (pseudosimultan, dacă există mai multe fragmente decât procesoare). De
exemplu, dacă dorim ca unele activităţi să fie îndeplinite în regim de fond sau programul să continue să reacţioneze la
unele evenimente exterioare în timpul îndeplinirii unor calcule foarte „costisitoare”. Pot fi aduse şi alte exemple.

60. Procese şi fire


Numim proces în Windows o instanţă (un exemplar) a programului, încărcat în memoria operativă. Această instanţă
poate crea fire (thread) - secvenţe de instrucţiuni, care urmează a fi executate. Este important să se înţeleagă că în
Windows anume firele sunt executate (nu procesele!), fiecărui proces fiindu-i asociat minimum un fir, numit firul
principal al aplicaţiei.
Deoarece în realitate există mult mai multe fire decât procesoare fizice, firele vor fi executate secvenţial, timpul de
procesor repartizându-se între fire. Dar viteza mare de execuţie şi frecvenţa mare de comutare a firelor lasă impresia
unei execuţii paralele a acestora.
Fără comentarii suplimentare subliniem, că stările elementare ale unui fir sunt aceleaşi ca şi în cazul proceselor:
ales (exe), eligibil (ready) şi blocat (wait). Starea blocat este asociată aşteptării unui anume eveniment. Când
evenimentul se produce firul este trecut în mod automat în starea eligibil. De exemplu, dacă un fir execută anumite
calcule, iar un alt fir este obligat să aştepte rezultatele pentru a le salva pe disc, al doilea fir ar putea utiliza un ciclu de
tipul "while(!isCalcFinished ) continue;". Este însă simplu să ne convingem, că în timpul execuţiei acestui ciclu
procesorul este ocupat 100% - este cazul aşteptării active. Astfel de cicluri trebuie evitate prin utilizarea mecanismelor
de sincronizare.
În cadrul sistemului de operare Windows există două tipuri de fire – fire interactive, care execută un ciclu propriu de
prelucrare a mesajelor (de exemplu, firul principal al unei aplicaţii) şi fire de lucru, care sunt funcţii simple. În ultimul
caz execuţia firului se încheie atunci când calculele, generate de funcţia respectivă, iau sfârşit.
Merită atenţie şi modalitatea organizării ordinii de execuţie a firelor. Algoritmul FIFO este departe de a fi cel mai
optimal. În Windows toate firele sunt ordonate conform priorităţilor. Prioritatea unui fir este un număr întreg de la 0 la
31 şi este determinată de prioritatea procesului, care a generat firul şi prioritatea relativă a firului. În acest mod se
ajunge la o flexibilitate maximă, fiecărui fir punându-i-se la dispoziţie – în caz ideal – exact atâta timp de procesor, cât
are nevoie.
Prioritatea firului poate fi modificată dinamic. Firele interactive, care au prioritatea Normal, sunt executate în mod
deosebit de către sistem, prioritatea acestor fire fiind majorată, atunci când procesul, care le-a generat, se află în
planul central (foreground). În rezultat, aplicaţia curentă reacţionează mai repede la cererile utilizatorului.

61. Necesitatea sincronizării


Când un proces este creat în mod automat este creat firul principal al acestuia. Acest fir poate crea în timpul execuţiei alte
fire, care la fel pot crea fire noi şi aşa mai departe. Timpul de procesor fiind repartizat între fire, fiecare fir „lucrează” în mod
independent.
Toate firele unui proces împart resursele comune, de exemplu, spaţiul de adrese al memoriei operative sau fişierele
deschise. Aceste resurse aparţin întregului proces, deci şi fiecărui fir. Fiecare fir poate să utilizeze aceste resurse fără nici un
fel de restricţii. În realitate, din cauza multitaskingului controlat (preemptive multitasking - la orice moment de timp sistemul
poate întrerupe execuţia unui fir şi transmite controlul unui alt fir), se poate întâmpla ca un fir să nu fi terminat încă lucrul cu
o resursă comună oarecare, iar sistemul să treacă la un alt fir, care utilizează aceeaşi resursă.
Rezultatele pot fi imprevizibile.
Asemenea conflicte se pot produce şi în cazul unor fire, care aparţin chiar unor procese diferite. Problema poate să apară
întotdeauna când două sau mai multe fire folosesc o resursă comună. Este necesar un mecanism de coordonare a lucrului
firelor cu resurse comune. În Windows acest mecanism se numeşte sincronizarea firelor (thread synchronization).
62. Structura mecanismului de sincronizare în Windows
Mecanismul de sincronizare este un set de obiecte ale sistemului de operare Windows, create şi gestionate program,
comune pentru toate firele sistemului (unele pentru firele unui singur proces) şi utilizate pentru coordonarea accesului la
resurse. În calitate de resurse pot fi toate obiectele, care pot fi accesate de două şi mai multe fire – un fişier pe disc, un port, un
articol al unei baze de date, o variabilă globală a unui program, accesibilă firelor unui singur procesor, un obiect al
dispozitivului interfeţei grafice (Graphic Device Interface), etc.
De obicei, sunt utilizate mecanismele (obiectele) de sincronizare, introduse mai sus: excluderea mutuală (mutex), secţia
critică (critical section), eveniment memorizat (event) şi semaforul (semaphore), fiecare realizând metoda proprie de
sincronizare. În calitate de obiecte sincronizate pot fi chiar procesele sau firele (când un fir aşteaptă terminarea execuţiei unui
proces sau a unui alt fir), fişierele, dispozitivele de comunicaţie, etc.
Sensul mecanismelor de sincronizare constă în faptul, că fiecare poate să fie în starea set. Pentru fiecare mecanism de
sincronizare această stare poate să aibă sens propriu. Firele pot să testeze starea curentă a mecanismului de sincronizare şi/sau
să aştepte modificarea acestei stări, coordonându-şi în acest fel acţiunile proprii. Este foarte important să se sublinieze, că
atunci când un fir lucrează cu mecanismele de sincronizare (le creează, le modifică starea) sistemul nu întrerupe execuţia
firului, până nu va fi terminată această acţiune, adică toate operaţiile finite din mecanismele de sincronizare sunt atomare (nu
pot fi întrerupte).
Menţionăm de asemenea, că nu există nici o legătură reală între mecanismele de sincronizare şi resurse.
Mecanismele de sincronizare nu pot interzice accesul nedorit la o resursă, ele doar indică firului momentul când acesta
poate accesa resursa, sau când acesta trebuie să aştepte (de exemplu, un semafor la o intersecţie doar indică când este permisă
trecerea. Cineva poate trece „pe roşu”, dar consecinţele pot fi grave).

63. Administrarea obiectelor de sincronizare în Windows


Crearea unui obiect de sincronizare se produce prin apelarea unei funcţii speciale din WinAPI de tipul Create… (de
exemplu, CreateMutex). Acest apel returnează descriptorul obiectului (handle), care poate fi folosit de toate firele procesului
dat. Un obiect de sincronizare poate fi accesat şi dintr-un alt proces, dacă acest proces a moştenit descriptorul obiectului dat,
sau folosind funcţia de deschidere a unui obiect (Open…). Obiectului, dacă el nu este destinat doar pentru uz intern (în
interiorul unui singur proces), în mod obligator i se acordă un nume unic. Nu poate fi creat un eveniment memorizat şi un
semafor cu acelaşi nume.
Folosind descriptorul poate fi determinată starea curentă a obiectului cu ajutorul funcţiilor de aşteptare. De exemplu,
funcţia WaitForSingleObject(x, y) cu doi parametri (primul este descriptorul obiectului, iar al doilea – timpul de aşteptare în
ms) returnează WAIT_OBJECT_0, dacă obiectul se află în starea set (adică nu aparţine nici unui fir şi poate fi utilizat pentru
sincronizare), WAIT_TIMEOUT – dacă a expirat timpul de aşteptare şi WAIT_ABANDONED, dacă obiectul de sincronizare
nu a fost eliberat înainte ca firul, care-l comanda, să se fi terminat. Dacă timpul de aşteptare este egal cu 0, atunci funcţia
returnează rezultatul imediat, în caz contrar, aşteaptă intervalul de timp indicat. În cazul în care starea obiectului de
sincronizare va deveni set până la expirarea acestui timp, funcţia returnează WAIT_OBJECT_0, altfel - WAIT_TIMEOUT.
Dacă în parametrul timp este indicată constanta simbolică INFINITE, funcţia va aştepta până când starea obiectului va
deveni set, fără vre-o restricţie.
Starea mai multor obiecte poate fi aflată cu ajutorul funcţiei WaitForMultipleObjects. Pentru încheierea lucrului cu un
obiect de sincronizare şi eliberarea descriptorului se apelează funcţia CloseHandle. Este important de ştiut, că apelarea unei
funcţii de aşteptarea blochează firul curent, adică atâta timp cât un fir se află în starea de aşteptare el nu are acces la procesor.

64. Excluderea mutuală


Cum a fost menţionat deja, mecanismele de excludere mutuală (mutex-ele, de la MUTual EXclusion) permit
coordonarea accesului la o resursă partajată. Starea set a obiectului corespunde momentului de timp în care obiectul nu
aparţine nici unui fir şi poate fi „utilizat”, iar starea reset – momentului când un fir oarecare controlează deja mutex-ul.
Accesarea va fi permisă doar după eliberare.
Pentru a lega mutex-ul de firul curent trebuie apelată una din funcţiile de aşteptare. Firul, căruia îi aparţine mutex-ul, îl
poate „ocupa” de mai multe ori, fără autoblocare, însă mai apoi acesta va trebui eliberat tot de atâtea ori cu ajutorul funcţiei
ReleaseMutex.
65. Evenimentele
Obiectele-evenimente sunt utilizate pentru a informa firele, care sunt în aşteptare, despre producerea unui eveniment. În
Windows există două tipuri de evenimente – cu resetare manuală şi automată. Resetarea manuală se execută cu funcţia
ResetEvent. Aceste evenimente sunt folosite pentru informarea mai multor fire, iar evenimentele cu resetare automată sunt
utilizate pentru informarea unui anumit fir, celelalte rămânând în aşteptare.
Funcţia CreateEvent crează un obiect-eveniment, funcţia SetEvent setează evenimentul în starea set, iar funcţia
ResetEvent resetează evenimentul. Funcţia PulseEvent setează evenimentul, iar după semnalizarea firelor, care erau în
aşteptare (toate în cazul resetării manuale şi doar unul la resetarea automată), resetează obiectul. Dacă nu există fire în
aşteptare, PulseEvent doar resetează obiectul, fără semnalizare.

66. Semafoarele
Un obiect-semafor este în ultimă instanţă un mutex cu contor. Acest obiect permite să fie „ocupat” de un număr anume
de fire, după care „ocuparea” va fi posibilă numai dacă unul din fire va „elibera” semaforul. Semafoarele sunt utilizate pentru
a limita numărul de fire, care lucrează simultan cu resursa. La iniţializare se specifică numărul maxim de fire, la fiecare
„ocupare” valoarea contorului semaforului scade.

67. Secţiunile critice


Obiectul-secţiune critică permite programatorului să evidenţieze un fragment de cod în care firul obţine acces la o resursă
partajată, preîntâmpinând utilizarea resursei de mai mulţi utilizatori. Pentru a utiliza resursa firul va intra mai întâi în secţiunea
critică (apelarea funcţiei EnterCriticalSection). Dacă intrarea a avut loc, nici un alt fir nu va avea acces la aceeaşi secţiune
critică, execuţia acestuia fiind suspendată. Reluarea se va produce în momentul în care primul fir părăseşte secţiunea critică
(funcţia LeaveCriticalSection). Diferenţa de mutex constă în faptul că secţiunea critică este utilizată numai pentru firele unui
singur proces.
Cu ajutorul funcţiei TryEnterCriticalSection se poate stabili, dacă secţiunea critică este liberă. Utilizând această
funcţie, un proces, fiind în aşteptarea resursei, poate să nu se blocheze, îndeplinind operaţii utile.

68. Protejarea accesării variabilelor


Există o serie de funcţii, care permit lucrul cu variabilele globale ale tuturor firelor, fără a ne preocupa de sincronizare,
deoarece aceste funcţii singure rezolvă problema sincronizării. Aceste funcţii sunt InterlockedIncrement,
InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd şi InterlockedCompareExchange. De
exemplu, funcţia InterlockedIncrement incrementează valoarea unei variabile pe 32 biţi cu o unitate.

69. Sincronizarea în Microsoft Fundation Classes


Biblioteca MFC conţine clase speciale pentru sincronizarea firelor (CMutex, CEvent, CCriticalSection şi
CSemaphore). Aceste clase corespund obiectelor de sincronizare WinAPI şi sunt derivate de la clasa CSyncObject.
Pentru utilizarea acestor clase trebuie consultaţi constructorii şi metodele lor – Lock şi Unlock. În principiu, aceste clase
sunt doar un fel de ambalaj pentru obiectele de sincronizare.
O altă modalitate de utilizare a acestor clase constă în crearea aşa numitelor clase thread-safe. O clasă thread-safe
reprezintă o anumită resursă în program. Tot lucrul cu resursa este realizat numai prin intermediul acestei clase, care conţine
toate metodele necesare. Clasa este proiectată în aşa mod, ca metodele ei să rezolve problema sincronizării, adică în cadrul
aplicaţiei să apară ca o simplă clasă. Obiectul de sincronizare MFC este inclus în această clasă în calitate de membru privat şi
toate funcţiile clasei, care realizează accesarea resursei, îşi coordonează lucrul cu acest membru.
Utilizând funcţiile Lock şi Unlock clasele de sincronizare MFC pot fi utilizate direct, iar în mod indirect – prin
funcţiile CSingleLock şi CmultiLock.
70. Exemplu de sincronizare în Windows
Prezentăm un exemplu simplu de lucru cu obiectul de sincronizare mutex [22]. Pentru simplitate a fost utilizată o
aplicaţie Win32 de consolă, deşi nu este obligator.

#include <windows.h>
#include <iostream.h>

void main()
{
DWORD res;

// creăm obiectul excludere mutuală


HANDLE mutex = CreateMutex(NULL, FALSE, "NUME_APLICATIE-MUTEX01");
// dacă obiectul există deja, CreateMutex va returna descriptorul obiectul existent,
// iar GetLastError va returna ERROR_ALREADY_EXISTS

// timp de 20 s încercăm să ocupăm obiectul


cout<<"Încerc să ocup obiectul...\n"; cout.flush();
res = WaitForSingleObject(mutex,20000);
if (res == WAIT_OBJECT_0) // dacă ocupare s-a terminat cu succes
{
// aşteptăm 10 s
cout<<"L-am prins! Aşteptare 10 secunde...\n"; cout.flush();
Sleep(10000);

// eliberăm obiectul
cout<<"Acum eliberăm obiectul\n"; cout.flush();
ReleaseMutex(mutex);
}

// închidem descriptorul
CloseHandle(mutex);
}
Pentru a controla modul de funcţionare a mecanismului de excludere mutuală se vor lansa două instanţe ale acestei
aplicaţii. Prima instanţă va ocupa obiectul imediat şi-l va elibera doar peste 10 secunde. Numai după aceasta instanţa
a doua va reuşi să ocupe obiectul. În acest exemplu obiectul de sincronizare este folosit pentru sincronizarea
proceselor, din care cauză în mod obligatoriu trebuie să aibă nume.

71. Utilizarea secţiunilor critice în Windows


În acest caz secţiunile critice sunt utilizate pentru a permite la un moment de timp dat accesul la unele date importante
unui singur fir al aplicaţiei, celelalte fire fiind blocate.
De exemplu, fie variabila m_pObject şi câteva fire, care apelează metodele obiectului referit de m_pObject.
Presupunem că această variabilă din timp în timp îşi poate schimba valoarea, valoarea 0 nu este interzisă. Fie următorul
fragment de cod:
// Firul #1
void Proc1()
{
if (m_pObject)
m_pObject->SomeMethod();
}

// Firul #2
void Proc2(IObject *pNewObject)
{
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
}
În acest exemplu există pericolul potenţial de apelare m_pObject->SomeMethod() după ce obiectul a fost distrus cu
ajutorul delete m_pObject, deoarece în sistemele de operare cu multitasking controlat execuţia oricărui fir poate fi
întreruptă în cel mai neconvenabil (pentru firul dat) moment şi să înceapă execuţia unui alt fir. Pentru exemplul nostru
momentul nedorit este atunci când firul #1 a testat deja m_pObject, dar nu a reuşit să apeleze SomeMethod().
Execuţia firului #1 a fost întreruptă şi a început execuţia firului #2. Iar firul #2 reuşise deja să apeleze destructorul
obiectului. Ce se va întâmpla atunci când firului #1 i se va acorda din nou timp de procesor şi va fi apelat
SomeMethod() al unui obiect deja inexistent? Greu de presupus.
Aici ne vin în ajutor secţiunile critice. Să modificăm exemplul de mai sus.

// Firul #1
void Proc1()
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
m_pObject->SomeMethod();
::LeaveCriticalSection(&m_lockObject);
}

// Firul #2
void Proc2(IObject *pNewObject)
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
::LeaveCriticalSection(&m_lockObject);
}

Fragmentul de cod inclus între ::EnterCriticalSection() şi ::LeaveCriticalSection() cu una şi aceeaşi secţiune


critică în calitate de parametru nu va fi executat nici o dată în mod paralel. Aceasta înseamnă, că dacă firul #1 a reuşit să
„acapareze” secţiunea critică m_lockObject, încercarea firului #2 să intre în aceeaşi secţiune critică va conduce la blocarea
acestuia până în momentul când firul #1 va elibera m_lockObject prin apelul ::LeaveCriticalSection(). Invers, dacă
firul #2 a accesat secţiunea critică înaintea firului #1, acesta din urmă va fi nevoit “să aştepte”, înainte de a începe lucrul cu
m_pObject.
Menţionăm, că secţiunile critice nu sunt obiecte ale nucleului sistemului de operare. Practic, tot lucrul cu secţiunile critice
are loc in procesul care le-a creat. Din aceasta rezultă, că secţiunile critice pot fi utilizate numai pentru sincronizare în cadrul
unui proces. Să cercetăm mai aprofundat o secţiune critică.

72. Structura RTL_CRITICAL_SECTION


Este definită după cum urmează:

typedef struct _RTL_CRITICAL_SECTION


{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // Folosit de sistemul de operare
LONG LockCount; // Contorul de utilizări
LONG RecursionCount; // Contorul accesării repetate din firul utilizatorului
HANDLE OwningThread; // ID firului utilizatorului (unic)
HANDLE LockSemaphore; // Obiectul nucleului folosit pentru aşteptare
ULONG_PTR SpinCount; // Numărul de cicluri goale înaintea apelării nucleului
}
RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

Câmpul LockCount este incrementat cu o unitate la fiecare apelare ::EnterCriticalSection() şi decrementat cu unu
la fiecare apel ::LeaveCriticalSection(). Acesta este primul (adesea şi unicul) control pentru testarea secţiunii critice. Dacă
după incrementare în acest câmp avem 0, aceasta înseamnă că până la acest moment nu au avut loc apeluri impare
::EnterCriticalSection() din alte fire.
În acest caz datele “păzite” de această secţiune critică pot fi utilizate în regim monopol. Adică, dacă secţiunea critică
este folosită intensiv de un singur fir, atunci ::EnterCriticalSection() se transformă practic în ++LockCount, iar
::LeaveCriticalSection() în--LockCount.
Aceasta înseamnă, că folosirea a mai multor mii de secţiuni critice într-un singur proces nu va conduce la creşterea
substanţială a utilizării nici a resurselor de sistem, nici a timpului de procesor. Ca şi concluzie: nu se va face economie pe
contul secţiunilor critice. Mult totuna nu vom putea economisi.
În câmpul RecursionCount este păstrat numărul de apeluri repetate ::EnterCriticalSection() din unul şi acelaşi fir.
Dacă se va apela ::EnterCriticalSection() din unul şi acelaşi fir de mai multe ori, toate apelurile vor avea succes. Adică
următorul cod nu se va opri pentru totdeauna în cel de-al doilea apel ::EnterCriticalSection(), ci va merge până la capăt.
// Firul #1
void Proc1()
{
::EnterCriticalSection(&m_lock);
// ...
Proc2()
// ...
::LeaveCriticalSection(&m_lock);
}

// Încă Firul #1
void Proc2()
{
::EnterCriticalSection(&m_lock);
// ...
::LeaveCriticalSection(&m_lock);
}
Într-adevăr, secţiunile critice sunt destinate să protejeze datele la accesarea din câteva fire. Utilizarea multiplă a uneia
şi aceeaşi secţiuni critice de un singur fir nu va genera eroare, ceea ce este normal. Trebuie doar să avem grijă ca numărul de
apeluri ::EnterCriticalSection() şi ::LeaveCriticalSection() să coincidă şi totul va fi în regulă.
Câmpul OwningThread conţine 0 în cazul secţiunilor critice libere sau identificatorul unic al firului-posesor. Acest
câmp este testat, dacă la apelul ::EnterCriticalSection() câmpul LockCount, după incrementarea cu o unitate, a devenit mai
mare ca 0. Dacă OwningThread coiincide cu identificatorul unic al firului curent, atunci valoarea lui RecursionCount
creşte cu o unitate şi ::EnterCriticalSection() este returnat imediat.
În caz contrar ::EnterCriticalSection() va aştepta până firul, care posedă secţiunea critică, va apela
::LeaveCriticalSection() de un număr necesar de ori. Câmpul LockSemaphore este folosit, dacă este necesar să se aştepte
până secţiunea critică este eliberată.
Dacă LockCount este mai mare ca 0 şi OwningThread nu coiincide cu identificatorul unic al firului curent, atunci
firul blocat crează un obiect al nucleului – eveniment – şi apelează ::WaitForSingleObject(LockSemaphore). Firul
posesor, după decrementarea câmpului RecursionCount, testează acest câmp şi dacă valoarea lui este 0, iar LockCount este
mai mare ca 0, constată că există minimum un fir în aşteptarea momentului când LockSemaphore se va afla în starea
“sosit”. Pentru aceasta firul-posesor apelează ::SetEvent() şi un fir oarecare (doar unul) dintre cele blocate este deblocat şi
are acces la datele critice.
Windows NT/2000 generează o excepţie, dacă încercarea de a crea un eveniment a eşuat. Aceasta este just atât pentru
funcţiile ::Enter/LeaveCriticalSection(), cât şi pentru ::InitializeCriticalSectionAndSpinCount() cu bitul cel mai
semnificativ al parametrului SpinCount setat. În cazul sistemului de operare WindowsXP creatorii nucleului au procedat un
pic altfel.
Aici, funcţiile ::Enter/LeaveCriticalSection() în loc să genereze o excepţie, dacă nu pot crea un eveniment propriu,
vor folosi un obiect global, unic pentru toţi, creat în avans. În aşa mod, în caz de deficienţă catastrofală a resurselor de sistem,
programul sub comanda lui WindowsXP “şchiopătează” un timp oarecare mai departe. Într-adevăr, este foarte complicat să
scrii programe, care ar fi în stare să continue lucrul după ce
::EnterCriticalSection() a generat o excepţie. De obicei, chiar dacă programatorul a prezis o astfel de situaţie, nu
se ajunge mai departe de generarea unui mesaj de eroare şi stoparea forţată a execuţiei programului. Drept rezultat,
WindowsXP ignorează bitul cel mai semnificativ al câmpului LockCount.
În sfârşit, câmpul LockCount. Acest câmp este folosit doar în sistemele multiprocesorale. În sistemele
monoprocesorale, dacă secţiunea critică este ocupată de un fir oarecare, putem doar comuta comanda la această secţiune şi
trece în aşteptarea evenimentului nostru.
În sistemele multiprocesorale există o alternativă: să executăm de câteva ori un ciclu vid, testând de fiecare dată dacă
secţiunea critică nu a fost eliberată (aşteptare activă). Dacă după un număr de SpinCount ori secţiunea critică nu a fost
eliberată, trecem în starea blocat. Această modalitate este mult mai eficientă decât comutarea la planificatorul nucleului şi
înapoi.
În WindowsNT/2000 bitul cel mai semnificativ al acestui câmp indică dacă obiectul nucleului, variabila handle a căruia
se află în câmpul LockSemaphore, trebuie creat anticipat.
Dacă pentru aceasta nu dispunem de resurse de sistem suficiente, sistemul generează o excepţie şi programul îşi poate
„micşora” posibilităţile funcţionale sau chiar termina execuţia.
73. Funcţii API pentru secţiunile critice
Descriem mai jos câteva funcţii din API pentru lucrul cu secţiunile critice.
Mai întâi funcţiile BOOL InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) şi BOOL
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount).
Completează câmpurile structurii lpCriticalSection adresate. După apelare secţiunea critică este gata de lucru. Iată
pseudocodul funcţiei RtlInitializeCriticalSection din ntdll.dll

VOID RtlInitializeCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
RtlInitializeCriticalSectionAndSpinCount(pcs, 0)
}

VOID RtlInitializeCriticalSectionAndSpinCount(LPRTL_CRITICAL_SECTION pcs,


DWORD dwSpinCount)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
pcs->LockSemaphore = NULL;
pcs->SpinCount = dwSpinCount;
if (0x80000000 & dwSpinCount)
_CriticalSectionGetEvent(pcs);
}

Funcţia DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD


dwSpinCount) setează valoarea câmpului SpinCount şi returnează valoarea precedentă a acestuia. Amintim, că bitul cel mai
semnificativ este responsabil de “legarea” evenimentului, folosit pentru aşteptarea accesului la secţiunea critică dată.
Pseudocodul funcţiei RtlSetCriticalSectionSpinCount din ntdll.dll este listat mai jos.

DWORD RtlSetCriticalSectionSpinCount(LPRTL_CRITICAL_SECTION pcs, DWORD dwSpinCount)


{
DWORD dwRet = pcs->SpinCount;
pcs->SpinCount = dwSpinCount;
return dwRet;
}

VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) eliberează resursele, ocupate de


secţiunea critică. Are următorul pseudocod:
VOID RtlDeleteCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
if (pcs->LockSemaphore)
{
::CloseHandle(pcs->LockSemaphore);
pcs->LockSemaphore = NULL;
}
}

VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection), BOOL


TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) permit intrarea în secţiunea critică. Dacă
secţiunea critică este ocupată de un alt fir, atunci ::EnterCriticalSection() va aştepta până aceasta va fi eliberată, iar
::TryEnterCriticalSection() va returna valoarea FALSE. Listingurile din ntdll.dll sunt:

VOID RtlEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
if (::InterlockedIncrement(&pcs->LockCount))
{
if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
pcs->RecursionCount++;
return;
}

RtlpWaitForCriticalSection(pcs);
}

pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}

BOOL RtlTryEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
{
pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
::InterlockedIncrement(&pcs->LockCount);
pcs->RecursionCount++;
}
else
return FALSE;

return TRUE;
}

VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) eliberează secţiunea critică.


Pseudocodul este următorul:
VOID RtlLeaveCriticalSectionDbg(LPRTL_CRITICAL_SECTION pcs)
{
if (--pcs->RecursionCount)
::InterlockedDecrement(&pcs->LockCount);
else if (::InterlockedDecrement(&pcs->LockCount) >= 0)
RtlpUnWaitCriticalSection(pcs);
}
74. Clase de secţiuni critice
Pentru o utilizare corectă a secţiunilor critice prezentăm mai jos codul claselor secţiunilor critice:
class CLock
{
friend class CScopeLock;
CRITICAL_SECTION m_CS;
public:
void Init() { ::InitializeCriticalSection(&m_CS); }
void Term() { ::DeleteCriticalSection(&m_CS); }

void Lock() { ::EnterCriticalSection(&m_CS); }


BOOL TryLock() { return ::TryEnterCriticalSection(&m_CS); }
void Unlock() { ::LeaveCriticalSection(&m_CS); }
};

class CAutoLock : public CLock


{
public:
CAutoLock() { Init(); }
~CAutoLock() { Term(); }
};

class CScopeLock
{
LPCRITICAL_SECTION m_pCS;
public:
CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
~CScopeLock() { Unlock(); }
void Lock() { ::EnterCriticalSection(m_pCS); }
void Unlock() { ::LeaveCriticalSection(m_pCS);
}
};

Clasele CLock şi CAutoLock sunt utilizate, de obicei, pentru sincronizarea accesării variabilelor clasei, iar
CScopeLock este destinat, în special, pentru a fi utilizată în proceduri. Compilatorul singur va avea grijă să apeleze
::LeaveCriticalSection() prin intermediul destructorului. Urmează un exemplu de folosire a CScopeLock.

CAutoLock m_lockObject;
CObject *m_pObject;
void Proc1()
{
CScopeLock lock(m_ lockObject); // apelarea lock.Lock();
if (!m_pObject)
return; // apelarea lock.Unlock();
m_pObject->SomeMethod();

// apelarea lock.Unlock();
}
75. Depanarea secţiunilor critice
Depanarea secţiunilor critice este o ocupaţie foarte interesantă, dar şi dificilă. Poţi căuta ore şi chiar zile în şir cauza
apariţiei unei probleme.
Erorile, legate de secţiunile critice sunt de două tipuri: de realizare şi de arhitectură. Erorile de realizare pot fi depistate
relativ uşor şi, de regulă, sunt generate de utilizarea incorectă (lipsa perechii) a apelurilor
::EnterCriticalSection() şi ::LeaveCriticalSection(). Urmează un fragment de cod în care este omis apelul
::EnterCriticalSection().
// În procedură se presupune, că m_lockObject.Lock(); a fost deja apelat
void Pool()
{
for (int i = 0; i < m_vectSinks.size(); i++)
{
m_lockObject.Unlock();
m_vectSinks[i]->DoSomething();
m_lockObject.Lock();
}
}
Apelul ::LeaveCriticalSection() fără ::EnterCriticalSection() va conduce la faptul că chiar primul apel
::EnterCriticalSection() va stopa execuţia firului pentru totdeauna.
În fragmentul de cod de mai jos lipseşte apelul ::LeaveCriticalSection():
void Proc()
{
m_lockObject.Lock();
if (!m_pObject)
return;
// ...
m_lockObject.Unlock();
}
În acest exemplu are sens să fie utilizată o clasă de tipul CSopeLock. Se mai poate întâmpla ca
::EnterCriticalSection() să fie apelată fără iniţializarea secţiunii critice cu ajutorul ::InitializeCriticalSection() (de
exemplu, în proiectele scrise cu ajutorul lui ATL). În versiunea debug totul poate lucra foarte bine, iar în versiunea
release „moare”.
Aceasta are loc din cauza aşa-zisului CRT (_ATL_MIN_CRT) „minimal”, care nu apelează constructorii obiectelor
statice (Q166480, Q165076). În versiunea ATL 7.0 această problemă a fost rezolvată. Pot să apară probleme, dacă atunci
când este folosită o clasă de tipul CScopeLock a fost omis identificatorul variabilei, de exemplu, CScopeLock (m_lock).
Compilatorul apelează constructorul CScopeLock şi imediat distruge acest obiect fără nume (în conformitate cu
standardul!). Adică, imediat după apelarea metodei Lock() are loc apelarea metodei Unlock() şi sincronizarea nu se
produce.
Dintre erorile de arhitectură cea mai frecventă este îmbrăţişarea fatală (deadlock, v.5.1.3.3), când două fire
încearcă să acceseze două şi mai multe secţiuni critice. Prezentăm un exemplu pentru două fire.
void Proc1()
// Firul #1
{
::EnterCriticalSection(&m_lock1);
// ...
::EnterCriticalSection(&m_lock2);
// ...
::LeaveCriticalSection(&m_lock2);
// ...
::LeaveCriticalSection(&m_lock1);
}

// Firul #2
void Proc2()
{
::EnterCriticalSection(&m_lock2);
// ...
::EnterCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock2);
}
Pot să apară probleme şi în cazul copierii unor secţiuni critice. Este greu de presupus că codul de mai jos a fost scris
de un programator sănătos:

CRITICAL_SECTION sec1;
CRITICAL_SECTION sec2;
// ...
sec1 = sec2;
Din atribuirea de mai sus este dificil să obţii foloase. Dar fragmentul următor poate fi adesea întâlnit:

struct SData
{
CLock m_lock;
DWORD m_dwSmth;
} m_data;

void Proc1(SData& data)


{
m_data = data;
}
şi totul ar fi OK, dacă structura SData ar avea on constructor de copiere, de exemplu:

SData(const SData data)


{
CScopeLock lock(data.m_lock);
m_dwSmth = data.m_dwSmth;
}
Presupunem că programatorul a considerat, că este suficient să se îndeplinească o simplă copiere a câmpurilor şi, în
rezultat, variabila m_lock a fost copiată, iar anume în acest moment ea fusese accesată dintr-un alt fir şi valoarea câmpului
LockCount este ≥ 0.
După apelul ::LeaveCriticalSection() din acel fir valoarea câmpului LockCount pentru variabila iniţială m_lock a
fost decrementată cu o unitate, pentru variabila copiată rămânând fără schimbare. Ca rezultat, orice apel
::EnterCriticalSection() nu se va întoarce niciodată în acest fir. Va rămâne pentru totdeauna în aşteptare. Pot exista situaţii
mult mai complicate.
Fie un obiect care apelează metodele unui alt obiect, obiectele aflându-se în fire diferite. Apelurile se fac în mod
sincron, adică obiectul #1 transmite execuţia firului obiectului #2, apelează metoda şi se va comuta înapoi la firul său. Execuţia
firului #1 va fi suspendată pentru toată perioada de execuţie a firului obiectului #2. Presupunem acum, că obiectul #2 apelează
o metodă a obiectului #1 din firul său.
Controlul va fi întors obiectului #1, dar din firul obiectului #2. Dacă obiectul #1 apelase metoda obiectului #2, intrând
într-o secţiune critică oarecare, atunci la apelarea metodei obiectului #1 acesta se va bloca pe sine însuşi la intrarea repetată în
aceeaşi secţiune critică. Fragmentul de cod care urmează vine să exemplifice această situaţie.
// Firul #1
void IObject1::Proc1()
{
// Intrăm în secţiunea critică a obiectului #1
m_lockObject.Lock();
// Apelăm metoda obiectului #2, are loc comutarea la firul obiectului #2
m_pObject2->SomeMethod();
// Aici nimerim numai după întoarcerea din m_pObject2->SomeMethod()
m_lockObject.Unlock();
}

// Firul #2
void IObject2::SomeMethod()
{
// Apelăm metoda obiectului #1 din firul obiectului #2
m_pObject1->Proc2();
}

// Firul #2
void IObject1::Proc2()
{
// Încercăm să intrăm în secţiunea critică a obiectului #1
m_lockObject.Lock();
// Aici nu vom ajunge niciodată
m_lockObject.Unlock();
}
Dacă în acest exemplu nu ar fi avut loc comutarea firelor, toate apelurile ar fi avut loc în firul obiectului #1 şi nu am
fi avut probleme. Exemple de acest gen stau la baza tehnologiei compartimentului COM (apartments).
Nu sunt recomandate apelurile obiectelor, dacă au avut loc intrări în secţiunile critice. Primul exemplu din acest
subparagraf va fi rescris astfel:
// Firul #1
void Proc1()
{
m_lockObject.Lock();
CComPtr<IObject> pObject(m_pObject); // apelarea pObject->AddRef();
m_lockObject.Unlock();
if (pObject)
pObject->SomeMethod();
}
// Firul #2
void Proc2(IObject *pNewObject)
{
m_lockObject.Lock();
m_pObject = pNewobject;
m_lockObject.Unlock();
}
Accesul la obiect a rămas ca şi mai înainte sincronizat, dar apelul SomeMethod() are loc în afara secţiunii critice.
Situaţia a fost aproape rezolvată. Mai există o problemă mică. Să cercetăm mai atent Proc2():

void Proc2(IObject *pNewObject)


{
m_lockObject.Lock();
if (m_pObject.p)
m_pObject.p->Release();
m_pObject.p = pNewobject;
if (m_pObject.p)
m_pObject.p->AddRef();
m_lockObject.Unlock();
}

Este evident, că apelurile m_pObject.p->AddRef() şi m_pObject.p->Release() au loc în interiorul secţiunii


critice. Şi dacă apelarea metodei AddRef() nu generează, de obicei probleme, apelarea metodei Release() poate fi ultimul
apel al Release() şi obiectul se va autodistruge.
În metoda FinalRelease() a obiectului #2 poate fi orice, de exemplu, eliberarea unor obiecte, care se află în alte
compartimente. Dar aceasta din nou va conduce la comutarea firelor şi poate genera autoblocarea obiectului #1 ca şi în
exemplul de mai sus. Pentru a preîntâmpina aceasta vom folosi aceeaşi tehnică ca şi în Proc1().
// Firul #2
void Proc2(IObject *pNewObject)
{
CComPtr<IObject> pPrevObject;
m_lockObject.Lock();
pPrevObject.Attach(m_pObject.Detach());
m_pObject = pNewobject;
m_lockObject.Unlock();
}
Potenţial, acum ultimul apel IObject2::Release() va fi executat după părăsirea secţiunii critice. Iar atribuirea unei
valori noi este sincronizată ca şi mai înainte cu apelul IObject2::SomeMethod() din firul #1.

Concluzii:
• secţiunile critice sunt executate relativ repede şi nu cer multe resurse de sistem;
• pentru sincronizarea accesării a mai multor variabile independente este mai bine să fie utilizate
câteva secţiuni critice (nu una pentru toate variabilele);
• codul unei secţiuni critice va fi redus la minimum;
• nu este recomandat să fie apelate metode ale unor obiecte “străine” dintr-o secţiune critică.

S-ar putea să vă placă și