Sunteți pe pagina 1din 35

3. Gestionarea activităţilor paralele ....................................................................................................................................

2
3.1. Exemple introductive ............................................................................................................................................... 2
3.1.1. Administrarea tamponată a intrărilor-ieşirelor ................................................................................................. 2
3.1.2. Comanda unui proces industrial ....................................................................................................................... 3
3.2. Noţiune de proces secvenţial ................................................................................................................................... 3
3.2.1. Proces unic. Context ......................................................................................................................................... 3
3.2.2. Relaţii între procese .......................................................................................................................................... 4
3.2.2.1. Mulţimi de procese. Paralelism ................................................................................................................. 4
3.2.2.2. Concurenţa proceselor. Resurse virtuale ................................................................................................... 5
3.2.2.3. Excludere mutuală ..................................................................................................................................... 6
3.3. Sincronizarea proceselor.......................................................................................................................................... 6
3.3.1. Exprimarea şi implementarea restricţiilor de precedare ................................................................................... 6
3.3.2. Probleme de realizare a sincronizării ................................................................................................................ 7
3.3.3. Monitorul – mecanism de sincronizare ............................................................................................................. 8
3.3.3.1. Definiţii...................................................................................................................................................... 8
3.3.3.2. Exemple de utilizare .................................................................................................................................. 9
3.4. Implementarea sincronizării .................................................................................................................................. 10
3.4.1. Probleme-tip ................................................................................................................................................... 10
3.4.2. Administrarea unei resurse partajate .............................................................................................................. 10
3.4.2.1. Alocarea resurselor banalizate ................................................................................................................. 10
3.4.2.2. Modelul cititorului şi redactorului ........................................................................................................... 12
3.4.3. Comunicarea între procese ............................................................................................................................. 13
3.4.3.1. Modelul producătorului şi consumatorului .............................................................................................. 13
3.4.3.2. Primitive de comunicare .......................................................................................................................... 14
3.4.3.3. Aplicaţii : relaţia client-server ................................................................................................................. 16
3.4.4. Administrarea intrărilor-ieşirilor .................................................................................................................... 16
3.4.4.1. Administrarea unui periferic .................................................................................................................... 16
3.4.4.2. Buferizarea imprimării ............................................................................................................................ 17
3.4.5. Sincronizare temporală ................................................................................................................................... 18
3.5. Gestionarea dinamică a proceselor ........................................................................................................................ 19
3.6. Sincronizarea în Windows ..................................................................................................................................... 20
3.6.1. Procese şi fire ................................................................................................................................................. 21
3.6.2. Necesitatea sincronizării ................................................................................................................................. 21
3.6.3. Structura mecanismului de sincronizare în Windows ..................................................................................... 21
3.6.4. Administrarea obiectelor de sincronizare în Windows ................................................................................... 22
3.6.4.1. Excluderea mutuală ................................................................................................................................. 22
3.6.4.2. Evenimentele ........................................................................................................................................... 22
3.6.4.3. Semafoarele ............................................................................................................................................. 22
3.6.4.4. Secţiunile critice ...................................................................................................................................... 22
3.6.4.5. Protejarea accesării variabilelor ............................................................................................................... 23
3.6.4.6. Sincronizarea în MFC .............................................................................................................................. 23
3.6.5. Exemplu de sincronizare în Windows ............................................................................................................ 23
3.6.6. Utilizarea secţiunilor critice în Windows ....................................................................................................... 23
3.6.6.1. Structura RTL_CRITICAL_SECTION ................................................................................................... 24
3.6.6.2. Funcţii API pentru secţiunile critice ........................................................................................................ 26
3.6.6.3. Clase de secţiuni critice ........................................................................................................................... 27
3.6.6.4. Depanarea secţiunilor critice ................................................................................................................... 28
3.7. Exerciţii la capitolul 3 ........................................................................................................................................... 31

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.1


3. Gestionarea activităţilor paralele
Una din caracteristicile principale ale sistemelor de calcul este existenţa activităţilor simultane. Capitolul 3 prezintă
conceptele şi instrumentele, care permit descrierea şi coordonarea acestor activităţi.

3.1. Exemple de activităţi paralele


3.1.1. Administrarea intrărilor-ieşirelor buferizate
Reluăm exemplul imprimării cu gestionarea unor zone tampon pe disc, descris în capitolul 2. Analiza acestui mod
de funcţionare (fig. 3.1) pune în evidenţă patru activităţi, care pot avea loc simultan:
1) primitiva scriere_linie SL (unitatea centrală)
2) scriere pe disc SD (canal 1)
3) citire de pe disc CD (canal 2)
4) imprimare fizică IF (canal 3)
scriere_linie
SL SD CD IF

tm1 tm2
(memorie) (memorie)
td

(disc)

Fig. 3.1. Gestionarea buferizată a unei imprimante


Aceste patru activităţi sunt în mare măsură autonome, or ele sunt executate pe procesoare distincte cu programe
diferite. Ele nu sunt totuşi independente, deoarece accesează obiecte comune: două zone tampon în memoria operativă,
tm1 şi tm2 şi una pe disc, td. Pot fi evidenţiate două tipuri de condiţii, care trebuie respectate:
1) Condiţii, care stabilesc posibilitatea existenţei activităţilor
O înregistrare nu poate fi preluată dintr-o zonă tampon înainte de a fi depozitată aici. Zonele tampon au capacităţi
limitate. Dacă o zonă tampon este total ocupată cu înregistrări, care nu au fost încă preluate, este imposibilă depozitarea.
Astfel, acţiunile de depozitare şi preluare sunt supuse unor condiţii de existenţă, formulate mai jos.
Activitate Acţiune Condiţie
SL scriere în tm1 tm1 nu este plin
SD citire din tm1 tm1 nu este vid
SD scriere în td td nu este plin
CD citire din td td nu este vid
CD scriere în tm2 tm2 nu este plin
IF citire din tm2 tm2 nu este vid
Derularea activităţilor modifică valorile de adevăr ale acestor condiții. De exemplu, imprimarea unei linii pune în
TRUE condiţia “tm2 nu este plin”.
O activitate, care nu poate executa o acţiune, deoarece condiţia asociată are valoare FALSE, trebuie să aştepte, adică
execuţia unei acţiuni trebuie întârziată până când valoarea logică a condiţiei devine TRUE. În capitolul 2 a fost discutată
realizarea acestui mecanism de aşteptare şi continuare cu ajutorul întreruperilor.
2) Condiţii de validitate a informaţiilor partajate
Dacă vom examina procesul de accesare a zonelor tampon vom descoperi o altă formă de interacţiune, cauzată de
posibilitatea de accesare simultană de către două activităţi a unui amplsament de memorie. Astfel, dacă SD citeşte
conţinutul unei înregistrări din tm1 pe care SL este în curs de a o modifica, rezultatul acestei citiri riscă să fie incoerent,
dacă nu vor fi luate măsuri speciale de precauţie. Problema poate fi rezolvată impunând una din activităţi, aflate în
conflict, să “aştepte” până când cealaltă va termina accesarea.
Concluziile acestei prime analize:
Lucrarea “imprimare buferizată” este realizată prin intermediul a patru activităţi simultane, în mare măsură
autonome, care cooperează pentru atingerea scopului final.


În realitate, citirea şi scrierea pe disc sunt executate pe acelaşi canal, ceea ce poate impune unele restricții suplimentare privind simultaneitatea
executării lor.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.2


Executarea corectă a lucrării impune respectarea anumitor restricții logice în derularea activităților. Aceste restricţii
pot conduce la întârzierea execuţiei unei activităţi, care este obligată să aştepte producerea unui eveniment, provocat de
o altă activitate.
3.1.2. Comanda unui proces industrial
Să reluăm exemplul din capitolul 1, în care un calculator comanda un reactor chimic. Considerăm două reactoare
identice, R1 şi R2, care funcţionează independent. Vom examina două soluţii posibile:
1) utilizarea a două calculatoare (câte unul pentru fiecare reactor),
2) folosirea unui singur calculator pentru comanda ambelor reactoare.
Prima variantă nu prezintă nimic nou în raport cu descrierea din capitolul 1. Soluţia a doua solicită identificarea unor
condiții suplimentare pentru realizarea sa. Fie P1, P2, D1, D2 segmentele procedurilor (programele) şi datelor pentru
comanda celor două reactoare R1 şi R2, memoria principală având capacitatea necesară pentru păstrarea acestor
segmente. Programele P1 şi P2 sunt executate pe rând de procesor. Raţionând ca şi în capitolul 1, concluzionăm că
relaţia 2t<T trebuie să fie respectată. Dacă acesta va fi cazul, funcţionarea reactoarelor pentru un observator extern pare
identică pentru ambele soluţii. Trebuie, totuşi să subliniem, că soluţia doi impune restricţii mai severe în ceea ce
priveşte performanţele calculatorului (capacitatea memoriei şi viteza de procesare). Să vedem acuma care sunt
modalitățile de implementare a soluţiei doi.
1) Partajarea procesorului
Între două execuţii succesive ale lui P1, procesorul este utilizat de P2, ca rezultat starea sa internă (conţinutul
registrelor, cuvântul de stare, contorul ordinal) va fi modificată. Pentru a permite reluarea lui P1, informaţiile sale
trebuiesc salvate la terminarea fiecărei secvenţe de execuţie şi restabilite la începerea execuţiei următoare. Aceleaşi
afirmaţii sunt valabile şi în cazul executării lui P2.
2) Partajarea programului
Programele P1 şi P2, fiind identice, putem păstra doar o singură copie (notată prin P) pentru economisirea memoriei.
Să examinăm condiţiile necesare pentru partajarea programului P:
 nu este permisă modificarea codului programului de execuţia sa,
 atunci când P este executat pentru reactorul Ri (i=1 sau 2), el va accesa segmentul de date Di; această adresare
selectivă a datelor va fi realizată de un mecanism care nu modifică textul programului.
Un program, existent în exemplar unic, dar care poate fi utilizat pentru executarea mai multor activităţi
independente, eventual simultane, se numeşte program reentrant (reenterabil). Acest mod de utilizare implică:
 invarianţa textului programului în cursul executării sale,
 desemnarea uniformă, de către program, a datelor proprii fiecărei activităţi.
Printre altele, dacă activităţile partajează un procesor unic, informaţiile despre starea acestuia (în particular, cele care
servesc la desemnarea datelor) trebuiesc salvate la fiecare comutare.
Să rezumăm rezultatele celui de-al doilea exemplu.
1. Au fost evidenţiate două activităţi logic independente: comanda reactoarelor R1 şi R2. Aceste două activităţi pot
fi puse pe două calculatoare distincte fără să existe vre-o legătură între ele.
2. Considerente de economie pot impune ca aceste activităţi să partajeze resurse fizice şi resurse program
comune. Buna funcţionare a acestei partajări restricţionează execuţia activităţilor (utilizarea alternativă a
procesorului) şi modul de utilizare a obiectelor partajate (salvarea contextului de executare, reenteranţa
programelor).
Ca şi concluzie pentru ambele exemple putem evidenţia două momente:
 existenţa unor activităţi evolutive, care pot derula simultan,
 existenţa unor relaţii între aceste activităţi: cooperare pentru executarea unor sarcini comune, concurenţă în
utilizarea resurselor partajate, relații determinate de specificaţiile de “bună funcţionare”. Ele se pot traduce
în restricţii de execuţie, care conduc la întârzieri temporare a progresării unei activităţi.
Continuarea acestui capitol are drept scop elaborarea unui suport formal pentru aceste noţiuni, introducând
conceptele de proces şi sincronizare şi descriind modalităţile lor de utilizare în cadrul unui sistem informatic.

3.2. Noţiune de proces secvenţial


Noţiunea de proces este mai întâi introdusă într-un mod concret pentru cazul unui calculator care conţine un
procesor unic şi o memorie adresabilă. Vom trece mai apoi la definiţia abstractă a unui proces independent de suportul
său fizic, ceea ce ne va permite introducerea problemei sincronizării.
3.2.1. Proces unic. Context
Noţiunea de proces pune la dispoziţie un model pentru reprezentarea unei activităţi, care rezultă din executarea unui
program pe calculator. Starea calculatorului este definită de starea procesorului (conţinutul registrelor) şi starea
memoriei (conţinutul amplasamentelor). Această stare este modificată de către procesor prin executarea instrucţiunilor

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.3


programului. Fiecare executare a unei instrucţiuni constituie o acţiune: o acţiune are ca efect trecerea în timp finit a
calculatorului dintr-o stare iniţială într-o stare finală. Acest efect este descris în specificaţiile instrucţiunii.
Executarea unei instrucţiuni va fi considerată indivizibilă sau atomară, adică este interzisă observarea sau definirea
stării maşinii în timpul acestei execuţii. Timpul utilizat pentru descrierea evoluţiei stării este un parametru t, care poate
lua o serie de valori discrete crescătoare, corespunzătoare începutului şi sfârşitului instrucţiunilor.  Aceste instanţe sunt
numite puncte de observare, iar stările corespunzătoare ale maşinii sunt numite puncte observabile. Prin abuz de
limbaj, acest ultim termen desemnează în egală măsură şi punctele de observare. Acestea permit să se asocieze data
numită valoarea curentă a lui t unei stări a mașinii – stare datată. O stare datată definește un eveniment. Evenimentele
permit reperarea modificărilor stării maşinii. Începutul şi sfârşitul unei acţiuni a sunt evenimente datele cărora vor fi
notate prin început(a) şi sfârşit(a); vom avea, evident, relația început(a) precede (<) sfârşit(a).
Astfel, executarea unui program se traduce într-o suită de acţiuni a1, a2,..., ai,..., cu început(ai) < sfârşit(ai). O
astfel de suită este numită proces secvenţial (sau simplu proces).
Un proces poate, deci, fi descris cu ajutorul succesiunii evenimentelor început(a1), sfârşit(a1), început(a2),
sfârşit(a2),... Această suită de stări datate ale maşinii se numeşte traiectorie temporală (sau istorie) a procesului. Putem
de asemenea defini un proces ca o suită de activităţi în sensul lui 2.1.
Mulţimea informaţiilor pe care acţiunile unui proces le pot consulta sau modifica se numeşte contextul procesului.
Reluând modelul de execuție secvențială din 2.1, contextul unui proces rezultant din executarea unui program conţine:
1) Contextul procesorului (cuvântul de stare şi registrele),
2) Un context în memorie - segmente procedură, date, stivă de execuție,
3) O mulţime de atribute ataşate procesului şi care specifică diferite proprietăţi:
a) Nume. Numele unui proces, care serveşte pentru identificarea lui, este de regulă, un număr intern atribuit la
crearea procesului şi care permite să se ajungă la reprezentarea contextului său (v. 4.2).
b) Prioritate. Prioritatea proceselor permite ordonarea lor pentru alocarea procesorului (v. 4.2, 4.3). Dacă
toate procesele au aceeaşi prioritate, alocarea se face conform ordinii “prim sosit, prim servit”.
c) Drepturi. Drepturile unui proces specifică operaţiile care îi sunt permise, în scopul asigurării protecţiei
informaţiei (v. 5.1.4).
Traiectoria temporală a unui proces este definită de şirul stărilor contextului său (procesor şi memorie) preluate după
execuţia fiecărei instrucţiuni. Prin definiţie, starea restului maşinii nu este modificată de execuţia procesului.
3.2.2. Relaţii între procese
Vom cerceta în continuare execuţia unei mulţimi de procese şi interacţiunea reciprocă a acestora. Noţiunile,
introduse în 3.2.1 pot fi extinse pentru o mulţime de procese:
 traiectoria temporală a unei mulţimi de procese este şirul evenimentelor formate de începuturile şi sfârşiturile
acţiunilor rezultante din executarea programelor acestor procese.
 contextele unor procese diferite pot avea părţi comune. Două procese, contextele cărora sunt disjuncte, se
numesc independente; ele nu pot avea interacţiuni reciproce. Partea contextului, care aparţine unui singur
proces, se numeşte context privat al procesului dat.
Exemplul 3.1. Fie două procese care partajează o procedură reentrantă. Segmentul executabil, care conţine această procedură aparţine contextului
comun. Segmentul de date şi stiva fiecărui proces formează contextul lor privat. ◄
3.2.2.1. Mulţimi de procese. Paralelism
Să considerăm două programe distincte P şi Q, fiecare având în memorie un segment de cod şi un segment de date.
Numim p şi q procesele rezultante din executarea acestor două programe. Executarea setului (p, q) poate să se producă
în diferite moduri, caracterizate de forma particulară a traiectoriei temporale. Trei posibile traiectorii sunt reprezentate
în figura 3.2. p q
(1)

p q p q p q
(2)

p q
(3)

Fig. 3.2. Executarea unei mulţimi de procese


Schemele de mai sus pot fi comentate astfel:


Într-un mod mai riguros ar trebui să adăugăm aici “punctele întreruptible” care pot fi prezente în unele instrucţiuni de lungă durată; o executare a
unei asemenea instrucţiuni poate fi considerată o suită de mai multe acţiuni.

Pentru comoditatea expunerii considerăm, că sfârşitul unei acţiuni şi începutul acţiunii următoare sunt două evenimente distincte, datele cărora sunt
diferite, deşi stările corespunzătoare sunt identice.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.4


 schema 1: este executat mai întâi procesul p în întregime, apoi procesul q la fel în totalitate,
 schema 2: sunt executate şiruri de instrucţiuni ale procesului p în mod alternativ cu şiruri de instrucţiuni ale
procesului q, şi tot aşa până la terminarea ambelor procese,
 schema 3: executarea proceselor p şi q este simultană; în acest caz sunt necesare două procesoare.
Pentru compararea acestor scheme de execuţie introducem noţiunea nivel de observare. Putem considera o suită de
acţiuni ale unui proces ca o acţiune unică, adică să observăm derularea unui proces considerând o unitate de execuţie
mai puţin fină decât instrucţiunea. De exemplu, dacă vom redefini noţiunea de acţiune elementară ca execuţie a unei
proceduri, traiectoria procesului va conţine doar stările definite de fiecare apel şi retur de procedură. Nivelul de
observare cel mai fin (cel al instrucţiunilor) este numit nivel de bază.
Să ne situăm mai întâi la nivelul de observare la care, prin convenţie, executarea completă a fiecărei dintre
programele P şi Q reprezintă o acţiune unică. Definiţiile care urmează sunt pentru acest nivel.
a) schema de tip 1 este a unei execuţii secvenţiale a lui p şi q. Ea este caracterizată de relaţiile:
sfârşit(q) < început(p) sau sfârşit(p) < început(q)
b) schemele de tip 2 sau 3 sunt scheme de execuţie paralelă. Ele sunt caracterizate de
sfârşit(p) > început(q) sau sfârşit(q) > început(p).
Revenim la nivelul de bază. Putem face o distincţie între schemele 2 şi 3. Într-adevăr, în schema 2, din considerente
de existenţă a unui singur procesor, la un moment de timp dat doar o singură acţiune poate fi executată, contrar schemei
3. Se va spune că în schema 3 are loc un paralelism real, iar în schema 2 – un pseudo-paralelism. Paralelismul real
solicită procesoare distincte. Două observaţii importante sunt necesare:
1) Diferenţa acestor scheme de execuţie este legată de alegerea nivelului de observare. Astfel, la nivelul de bază,
diferenţa dintre schemele 1 şi 2 dispare: ambele sunt secvenţiale.
Exemplu 3.2. Utilizatorii unui sistem de operare, care funcţionează în timp partajat pe un singur procesor, au impresia că programele lor sunt
executate în mod paralel, deoarece nivelul lor de observare este cel al comenzilor limbajului de comandă, compuse din mai multe
instrucțiuni. La nivelul de bază, însă, aceste instrucţiuni sunt atomare şi executate de procesor în mod secvenţial. Exemplul celor
două reactoare chimice, prezentate în 3.1.2, conduce la observaţii analogice. ◄

2) Alegerea nivelului de observare depinde de finețea fenomenelor, care dorim să le considerăm elementare. Dacă
trebuie să studiem executarea instrucţiunilor în “pipe-line” pe un procesor microprogramat, în calitate de nivel
de observare va fi ales nivelul microinstrucţiunilor, iar contextul va fi completat cu memoria microprogramelor
și registrele interne.
Vom examina realizarea, la nivelul de bază, a unei scheme de execuţie de tipul 2. La fiecare realocare a
procesorului, contextul procesului curent trebuie salvat, pentru a permite reluarea ulterioară a acestei execuţii. Dacă
memoria are o capacitate suficientă pentru a păstra toate segmentele, doar contextul procesorului trebuie salvat. Dacă
memoria principală poate conţine, la un moment de timp dat, doar segmentul procedură şi datele unui singur proces,
aceste segmente de asemenea trebuie salvate pe disc. Această remarcă justifică definiţia operaţională, adesea întâlnită,
a contextului unui proces ca mulţime a informaţiilor care trebuie salvată pentru a permite reluarea ulterioară a
procesului, dacă execuţia acestuia a fost întreruptă.
3.2.2.2. Concurenţa proceselor. Resurse virtuale
Situaţia descrisă de schemele 1 şi 2 nu rezultă dintr-o legătură logică între p şi q, ci doar din existenţa unui singur
procesor. Ea poate fi caracterizată astfel: fie o mulţime de procese contextele cărora au un obiect comun, care poate fi
utilizat la un moment de timp dat de un singur proces. Se va spune în acest caz, că obiectul constituie o resursă critică
pentru procesele date sau că procesele sunt în excludere mutuală (excludere reciprocă sau concurenţă) pentru
utilizarea unei resurse. În situaţia descrisă, procesorul este o resursă critică pentru procesele p şi q.
Observăm că excluderea mutuală a unei resurse conduce la “serializarea” execuţiei proceselor concurente, în cazul
unor acţiuni, care cer această resursă (în exemplul dat, toate acţiunile). Schemele 1 şi 2 diferă doar prin nivelul de fineţe
la care este executată serializarea.
Funcţionarea corectă a unei mulţimi de procese, care participă la îndeplinirea unei lucrări comune, implică relaţii
logice de cooperare (v.3.1.1). Este comod să se separe această cooperare de concurenţa pentru resursele fizice cu
scopul de a simplifica înţelegerea şi aplicarea celor două tipuri de relaţii. Pentru aceasta este folosită noţiunea de
resurse virtuale: fiecărei resurse fizice critice i se asociază tot atâtea copii imaginare (sau virtuale) ale acestei resurse
câte procese concurente solicită utilizarea ei. Suntem nevoiţi să tratăm două probleme distincte:
1) respectarea relaţiilor de cooperare între procesele, care, fiecare posedă (conceptual) resursele fizice solicitate şi
pentru care paralelismul în execuţie nu este restricţionat de competiţia pentru resurse,
2) reglarea problemei de concurenţă pentru resursele fizice printr-o serializare convenabilă a execuţiei proceselor
în cauză. Se va spune în acest caz, că realizăm o alocare a resurselor fizice.
Introducerea resurselor virtuale are o consecinţă foarte importantă pe care o vom ilustra-o prin exemplul proceselor
p şi q, definite în 3.2.2.1. Să ataşăm fiecărui proces un procesor virtual. Conceptual, totul va avea loc ca şi cum
procesele s-ar derula paralel, conform unei scheme, numite logice sau virtuale, analogice schemei 3 din fig.3.2. Cu toate
acestea, trebuie de menţionat, că această schemă logică reprezintă doar o notaţie compactă pentru mulţimea schemelor
reale posibile şi că ele sunt obligatoriu de forma 1 sau 2 din considerentele unicităţii procesorului. Pentru o schemă

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.5


reală şi una virtuală a unui proces dat este păstrată doar ordinea de succesiune a evenimentelor (începutul şi sfârşitul
acţiunii) şi nu sunt păstrate valorile absolute ale intervalelor de timp, care le separă. În absenţa altor informaţii, nu
putem spune nimic apriori despre ordinea evenimentelor, asociate unor procese distincte. Timpul folosit la reperarea
evenimentelor în schema logică este numit timp logic; relaţiile sale cu timpul real sunt prezentate în fig.3.3.
a1 a2
+------+-----------------+------+ procesul p
b1 b2 (timp logic)
+--+-------+---------+ procesul q
a1 a2
+------+---+ +----------+ +----+------+ p (timp real, execuţia 1)
+--+--+ +-----+---------+ q
b1 b2
a1 a2
+---+ +---+-----------------+------+ p (timp real, execuţia 2)
+--+----+ +---+---------+ q
b1 b2
În toate cazurile a1 precede a2, b1 precede b2.
Fig.3.3. Timpul logic şi timpul real
Vom considera, că evoluţia proceselor are loc în timp logic, adică vor fi interzise orice ipoteze despre viteza relativă
a proceselor în cauză: aceasta este consecinţa simplificării considerabile, introduse de noţiunea de resurse virtuale, care
permite ignorarea mecanismelor de alocare. O singură excepţie, totuşi va fi făcută pentru studierea sincronizării
temporale, în care timpul fizic intervine în mod precis ca măsură a duratei şi nu numai ca mod de reperare relativă.
3.2.2.3. Excludere mutuală
Introducerea resurselor virtuale ne permite să tratăm problema excluderii mutuale la accesarea resurselor fizice
corespunzătoare în cadrul mecanismelor de alocare. Această problemă poate fi pusă într-un alt mod cum o
demonstrează exemplul următor.
Exemplu 3.3. Două procese p şi q trebuie fiecare să actualizeze valoarea unei variabile comune n (de exemplu, n este starea unui cont bancar
pentru care p şi q efectuează o depozitare). Acţiunile respective se vor scrie în programele lui p şi q:
procesul p: Ap : n=n+np procesul q: Aq : n=n+nq
Să realizăm decompoziţia acestor acţiuni în instrucţii, notând prin Rp şi Rq registrele locale ale proceselor p şi q respectiv:
procesul p procesul q
1. load Rp, n 1’. load Rq, n
2. add Rp, np 2’. add Rq, nq
3. sto Rp, n 3’. sto Rq, n
Dacă aceste acţiuni sunt executate în ordinea 1, 1 ’, 2, 2’, 3, 3’, efectul lor global este de executat n:=n+nq şi nu n:=n+np+nq: una
din actualizări a fost pierdută şi valoarea finală a variabilei n este incorectă. Pentru evitarea acestei incoerenţe acţiunile Ap şi Aq
trebuie să fie executate în excludere reciprocă; se zice de asemenea că ele constituie pentru p şi q secţiuni critice. Este necesar să se
respecte condiţia
sfârşit(Ap) < început(Aq) sau sfârşit(Aq) < început(Ap).
Această condiţie de serializare, care are efectul de a face Ap şi Aq să devină atomare, este identică condiţiei deja întâlnite în 3.2.2.2
la accesarea unei resurse fizice critice. ◄

Excluderea mutuală constă în extinderea pentru secvenţa de acţiuni a proprietăţii de indivizibilitate a acţiunilor
nivelului de bază (acţiuni atomare). Posibilitatea executării unor acţiuni atomare se află la baza mecanismelor care
realizează sincronizarea.

3.3. Sincronizarea proceselor


3.3.1. Exprimarea şi implementarea restricţiilor de succesiune
Introducem două exemple, care ne vor ajuta la formularea restricţiilor logice impuse de cooperare.
Exemplul 3.4. Procesul p transmite informaţii procesului q scriind într-un segment a, consultat de q (se presupune că această transmitere are loc o
singură dată). Este necesar să se verifice condiţia:
sfârşit(scriere(a)) < început(citire(a))
Această relaţie exprimă restricţia, că citirea lui a de către q nu poate începe înainte de terminarea scrierii lui a de către p. ◄
Exemplul 3.5. Rendez-vous. Fie N procese p1,..., pN. Definim în programul fiecărui proces un punct, numit rendez-vous, pe care procesul nu-l poate
trece înainte ca alte procese să ajungă la punctul lor propriu de rendez-vous.
Dacă programul procesului pi are forma
<debut_i>;

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.6


<rendez-vous>
<continuare_i>;
atunci restricţiile de sincronizare se vor exprima după cum urmează:
 i, j  [1..N]: sfârşit(debut_i) < început(continuare_j) ◄
Restricţiile de sincronizare pot fi exprimate prin următoarele două forme echivalente:
1. Se va impune o ordine de succesiune în timp logic pentru unele puncte ale traiectoriei temporale ale procesului,
2. Se va impune unor procese o condiţie de autorizare a depăşirii acestor puncte ale traiectoriei lor temporale.
Punctele privilegiate astfel se vor numi puncte de sincronizare.
Expresia (2) arată, că restricţiile de sincronizare pot fi satisfăcute impunând un proces “să aştepte” să execute o
acţiune până când o oarecare condiţie va fi satisfăcută. Această noţiune de aşteptare nu poate fi exprimată cu ajutorul
instrumentelor introduse până acum. Pentru aceasta vom introduce o nouă stare pentru un proces, stare în care procesul
se zice în aşteptare sau blocat, prin opoziţie stării activ, considerate până acum în mod implicit. Un proces, care intră
în starea de aşteptare, plecând de la un punct observabil t, opreşte progresarea începând cu acest punct şi stopează
executarea acţiunilor. În momentul în care procesul revine în starea activ, el reia execuţia sa şi contextul său privat
restabileşte starea pe care procesul o avea în punctul t (partea neprivată a contextului poate fi modificată de execuţia
altor procese).
Se numeşte blocare tranziţia activaşteptare şi deblocare tranziţia inversă.
Vom utiliza noţiunea de aşteptare pentru specificarea sincronizării proceselor. Această specificare se va produce în
două etape:
1) definirea punctelor de sincronizare pentru fiecare proces,
2) asocierea unei condiţii de depăşire fiecărui punct de sincronizare, condiţie exprimată prin intermediul
variabilelor de stare a sistemului.
Vom ilustra acest mod de specificare pentru cele două exemple precedente. Notăm un punct de sincronizare prin ps,
iar condiţia de depăşire asociată prin aut(ps). Doar dacă această condiţie are valoarea TRUE, procesul în cauză este
autorizat să execute instrucţiunea etichetată cu ps.
Exemplul 3.6: var f : boolean init false
procesul p procesul q
scriere(a); <debut_q>;
f:=true;
<continuare_p> ps : citire(a)

aut(ps) : f:=true
A fost introdusă variabila de stare f pentru a exprima condiţia “procesul p a terminat acţiunea scriere(a)”.◄
Exemplul 3.7: var n : integer init 0;
procesul pi
<debut_i>;
n:=n+1
ps: <continuare_i>
aut(psi): n=N (i=1,...,N);
Variabila de stare n este în acest caz numărul procesului sosit în punctul de rendez-vous.◄

3.3.2. Probleme de realizare a sincronizării


Vom încerca să implementăm sincronizarea specificată de condiţiile de depăşire. Pentru aceasta trebuie să definim
un mecanism de aşteptare prin introducerea noţiunii de eveniment memorizat. Un eveniment memorizat (e) este o
variabilă, care poate lua două valori: TRUE şi FALSE, valoarea iniţială FALSE. Asupra evenimentului memorizat sunt
posibile două operaţii, care sunt acţiuni indivizibile:
e:=<valoare> -- atribuirea imediată a unei valori
aşteptare(e).
Operaţia aşteptare(e), executată de un proces p, are următoarea specificaţie:
if e = FALSE then
starea(p) := blocat -- p este trecut în “aşteptarea lui e”
endif
Când e ia valoarea TRUE, toate procesele care aşteptau e trec în starea activ.
Vom încerca acum să realizăm cu ajutorul acestui mecanism sincronizarea pentru cele două exemple.
Exemplul 3.8. var e : eveniment memorizat;
procesul p procesul q
scriere(a); <debut_q>;
e:=TRUE; aşteptare(e);
<continuare_p> citire(a) ◄

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.7


Analiza acestui sistem (care nu conţine alte variabile de stare, decât evenimentul memorizat e) poate fi făcută
enumerând traiectoriile temporale posibile. Această analiză arată, că sincronizarea este corectă atunci când operaţiile
asupra evenimentului memorizat sunt indivizibile.
Exemplul 3.9. var e : eveniment memorizat;
n : integer init 0;
procesul pi
<debuti>;
(A) n:=n+1;
(B) if n<N then
aşteptare(e)
else
e:=TRUE
endif
<continuarei> ◄
O analiză mai atentă ne arată că acest program nu este tocmai corect. Notând un registru local al procesului i prin Ri,
acţiunile (A) şi (B) pot fi descompuse după cum urmează:
(1) load Ri, n
(2) ai Ri, 1 -- adunare imediată
(3) ci Ri, N --comparare imediată
(4) br () etichetă -- salt dacă Ri  N
<aşteptare (e)>
...
etichetă : ...
Presupunem, că toate proceselor sunt în aşteptare în punctele lor de rendez-vous, cu excepţia a două, notate prin pj şi
pk. Dacă pj şi pk vor fi executate pe traiectoria temporală (1j, 1k, 2j,...), atunci fiecare va atribui lui n valoarea finală N-1
şi se va bloca, drept rezultat toate procesele se vor bloca pentru un timp indefinit.
Reieşind din analiza de mai sus putem concluziona, că implementarea condiţiilor de sincronizare nu poate fi corect
realizată numai cu ajutorul operaţiilor de aşteptare. Consultarea şi modificarea variabilelor de stare, care intervin în
aceste condiţii, trebuie să fie executate în regim de excludere reciprocă. Observaţia dată ne impune să introducem un
mecanism de sincronizare, care în mod automat ar asigura acest regim de funcţionare.
3.3.3. Monitorul – mecanism de sincronizare
3.3.3.1. Definiţii
Un monitor este constituit dintr-o mulţime de variabile de stare şi o mulţime de proceduri, care utilizează aceste
variabile. Unele dintre aceste proceduri, numite externe, sunt accesibile utilizatorilor monitorului. Numele acestor
proceduri sunt numite puncte de intrare ale monitorului. Procesele, care utilizează monitorul pentru a se sincroniza, nu
au acces direct la variabilele de stare. Monitorul poate fi utilizat doar prin apelarea procedurilor sale externe. Acestea
permit blocarea sau deblocarea proceselor conform specificaţiilor problemei. Condiţiile de blocare sau deblocare sunt
exprimate ca funcţie ale variabilelor de stare, iar mecanismul de execuţie al monitorului garantează manipularea acestor
variabile în regim de excludere mutuală. În fine, un monitor conţine un fragment de cod de iniţializare, executat o
singură dată la crearea monitorului.
Blocarea şi deblocarea proceselor se exprimă, în procedurile monitorului, prin intermediul unor condiţii. O condiţie
este declarată ca şi o variabilă, dar nu are “valoare”: o condiţie c poate fi utilizată doar prin intermediul a trei operaţii
sau primitive, efectul cărora este descris mai jos (prin p am notat procesul, care le execută)
c.aşteptare : blochează procesul p şi îl plasează în “aşteptarea lui c”
c.vid : funcţie cu valori booleene (true, dacă nu există nici un proces în aşteptarea lui c, altfel false)
c.semnalizare : if non c.vid then <deblocarea proceselor care sunt în aşteptarea lui c> endif
Procesele, care sunt în aşteptarea unei condiţii c, sunt grupate într-un fir de aşteptare asociat lui c. Putem spune, că o
condiţie furnizează proceselor un instrument de desemnare a unui astfel fir de aşteptare.
Un proces deblocat de c.semnalizare îşi reia execuţia de la instrucţiunea, care urmează imediat primitivei
c.aşteptare, care-l blocase. Necesitatea asigurării excluderii reciproce pentru variabilele de stare impune o restricţie
suplimentară mecanismelor de deblocare: atunci când un proces p deblochează un proces q printr-o operaţie de
semnalizare, p şi q nu pot fi menţinute simultan active. Se va impune blocarea lui p până în momentul în care q, la
rândul său, se va bloca sau va părăsi monitorul. Pentru evitarea unei blocări indefinite a lui p este necesar ca operaţia de
transfer a comenzii de la p la q să fie atomară (indivizibilă) şi se va garanta, că un proces care a fost temporar blocat
este deblocat de operaţia semnalizare înainte ca un nou proces să poată executa o procedură a monitorului.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.8


Observaţie. Problema excluderii mutuale , legată de primitiva semnalizare nu se va pune, dacă aceasta este ultima operaţie într-o
procedură a monitorului. Acest caz este foarte frecvent în practică, unele monitoare (de exemplu, Concurrent Pascal) respectă în mod
obligator restricţia dată.
3.3.3.2. Exemple de utilizare
Exemplul 3.10. sinc: monitor;
var fact: boolean;
term: condiţie;
procedura terminare_scriere;
begin
fact:=true;
term.semnalizare
end
procedura debut_citire;
if non fact then
term.aşteptare
endif
begin -- iniţializare
fact := false
end
end sinc
Monitorul este utilizat după cum urmează:
procesul p procesul q
scriere(a); <debut_q>
sinc.terminare_scriere; sinc.debut_citire;
<continuare_p> citire(a);
Exemplul 3.11. rendez-vous : monitor;
var n : integer;
toţi_sosiţi : condiţie;
procedura sosire;
begin
n := n+1;
if n < N then
toţi_sosiţi.aşteptare --nu au sosit toate procesele
endif
toţi_sosiţi.semnalizare -- a sosit şi ultimul
end
begin -- iniţializare
n:=0
end
end rendez_vous
Programul procesului pi va fi de forma:
procesul pi
<debut i>
rendez_vous.sosire;
<continuare i>
Este foarte important să înţelegem funcţionarea sincronizării: procesul, care soseşte ultimul la rendez_vous, execută
toţi_sosiţi.semnalizare şi deblochează unul dintre procesele, care sunt în starea aşteptare. Acesta, la rândul său, execută
toţi_sosiţi.semnalizare, deblocând următorul proces şi aşa mai departe.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.9


3.4. Implementarea sincronizării
3.4.1. Probleme-tip
Experienţa demonstrează, că problemele de sincronizare logică întâlnite în practică pot fi reduse, în marea lor
majoritate, la combinaţia unui număr mic de situaţii elementare, schemele de soluţionare ale cărora sunt cunoscute.
Secţiunile 3.4.2 – 3.4.5 sunt consacrate studierii acestor probleme-tip, utilizând instrumentarul de bază, pus la dispoziţie
de monitoare. Problemele-tip sunt următoarele:
 accesarea de către o mulţime de procese a unei resurse partajate comune,
 comunicarea între procese,
 gestionarea perifericelor şi intrărilor-ieşirilor tamponate,
 sincronizare temporală.
3.4.2. Administrarea unei resurse partajate
Considerăm o resursă (fizică sau logică) partajată de o mulţime de procese. Utilizarea acestei resurse trebuie să
respecte nişte reguli de utilizare, destinaţia cărora constă în garantarea unor proprietăţi specificate sau restricţii de
integritate. Aceste restricţii sunt specificate pentru fiecare resursă. O modalitate de garantare a respectării regulilor de
utilizare a unei resurse constă în adoptarea următoarei scheme:
 modul de folosire a resursei presupune utilizarea obligatorie a procedurilor de acces asociate resursei; orice
tentativă de utilizare, care nu respectă acest mod este detectată automat,
 procedurile de accesare sunt grupate într-un monitor, sau mai multe, programul căruia impune respectarea
restricţiilor de integritate.
Cel mai simplu caz este acela al unei resurse pentru care singura restricţie de integritate este de a fi utilizată în
excludere reciprocă. Simpla grupare a procedurilor sale de acces într-un monitor unic garantează respectarea acestor
restricţii.
3.4.2.1. Alocarea resurselor banalizate
Considerăm o resursă pentru care există un număr fix de N exemplare. Un proces poate accesa la cerere n unităţi din
cele N, le poate utiliza şi apoi elibera. O unitate, folosită de un proces, se numeşte alocată procesului, care o utilizează,
pentru toată perioada de la accesare până la eliberare. Toate unităţile sunt echivalente din punctul de vedere al
proceselor utilizatoare, vom mai zice că resursa este banalizată. Zonele-tampon din memoria principală sau pe disc,
unităţile de bandă magnetică, etc. sunt exemple de resurse banalizate.
Vom admite următoarele reguli de utilizare:
 o unitate poate fi alocată la un moment de timp dat doar unui singur proces,
 o unitate poate fi alocată unui proces, care cere alocarea, doar dacă ea este liberă (nu este alocată nici unui
proces),
 o operaţie de eliberare este aplicată totdeauna ultimelor resurse, achiziţionate de procesul care execută
eliberarea,
 o cerere de alocare este blocantă în caz de eşec (număr insuficient de unităţi libere).
Definim două proceduri cerere şi eliberare ale unui monitor. Utilizarea resursei are loc conform schemei de mai
jos.
ps:resurse.cerere(n); -- cerere pentru n unităţi
-- aşteptare în caz de eşec
<utilizarea unităţilor primite>
resurse.eliberare(n) -- eliberarea resurselor
Condiţia de sincronizare se va scrie pentru orice proces:
aut(ps) : n  nlibere
Prima formă a unui monitor resurse se va obţine utilizând direct condiţia de sincronizare:
resurse: monitor;
var nlibere : integer;
disp : condiţie;
procedura cerere(n);
begin
while n>nlibere do

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.10


disp.aşteptare;
endwhile;
nlibere:=nlibere-n
end;
procedura eliberare(n);
begin
nlibere:=nlibere+n;
disp.semnalizare -- deblocare în lanţ
end;
begin -- iniţializare
nlibere:=N
end
end resurse
Nu a fost făcută nici o ipoteză despre ordinea proceselor în firul de aşteptare al condiţiei disp. Drept consecinţă, la
fiecare eliberare vor fi deblocate toate procesele. Este o soluţie greoaie şi poate fi foarte costisitoare, dacă există multe
procese. Este preferabil să se programeze în mod explicit administrarea firului de aşteptare a cererilor, ceea ce obligă să
avem câte o condiţie distinctă pentru fiecare proces.
Pentru elaborarea programelor vom introduce un tip discret proces, variabilele căruia permit desemnarea proceselor.
resurse : monitor
type element : struct
lungime : integer
proc : proces
end;
var nlibere : integer;
disp :array[proces] of condiţie;
<declaraţia firului de aşteptare f şi procedurilor sale de accesare: introducere, extragere, primul>
procedura cerere(n);
begin
var e: element;
if n>nlibere then
e.lungime:=n;
e.proc:=p; -- p este procesul apelant
introducere(e,f); -- în ordinea de creştere a lungimii
disp[p].aşteptare
endif;
nlibere:=nlibere-n
end;
procedura eliberare(n);
var e: element;
begin
nlibere:=nlibere+n;
while primul(f).lungime  nlibere do
extragere(e,f); -- elementul extras = primul (f)
disp[e.proc].semnalizare -- e.proc = procesul deblocat
endwhile;
end;
begin -- iniţializare
nlibere:=N;
f:=<vid>
end
end resurse
Această soluţie este mult mai compactă şi mai generală, deoarece permite o mai bună separare a expresiei
sincronizării (ceea ce rezultă din structura monitorului) de politica de alocare (care este definită de procedurile de
gestionare a firului de aşteptare introducere şi extragere).

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.11


3.4.2.2. Modelul cititorului şi scriitorului
Să considerăm un fişier manipulat de procese din două clase diferite: cititori, care consultă fişierul fără a modifica
conţinutul lui şi scriitori, care pot modifica acest conţinut. Fie pentru un moment arbitrar de timp ncit şi nscr numărul
de cititori şi de scriitori, respectiv, care folosesc o procedură de acces la fişier. Cererea de asigurare a coerenţei
fişierului ne impune să respectăm următoarele restricţii:
(nscr=0) şi (ncit0) -- fişier în citire
sau (nscr =1) şi (ncit=0) -- fişier în scriere
Fie fich un monitor care asigură respectarea acestor restricţii. Vom impune următoarea formă a acceselor la fişier:
proces cititor proces scriitor
fich.debut_citire; fich.debut_scriere;
<acces citire> <acces scriere>
fich.terminare_citire; fich.terminare_scriere;
Procedurile debut_citire, terminare_citire, debut_scriere, terminare_scriere trebuie să asigure respectarea
restricţiilor de mai sus. Vom implementa aceste restricţii autorizând depăşirile; pentru aceasta este necesar să fie
precizate priorităţile între cititori şi scriitori.
Cu titlu de exemplu, presupunem că cititorii au prioritate în faţa redactorilor (o scriere nu va fi autorizată, dacă
există cititori în aşteptare). Definim următoarele variabile de stare:
scr = o scriere este în curs (valoare booleană)
nc = numărul de cititori în aşteptare sau în curs de citire
În acest caz, condiţiile de depăşire se vor exprima după cum urmează:
aut(citire) : scr=false (nu există scrieri curente)
aut(scriere): scr=false şi nc=0 (nici scrieri nici citiri în curs, nici cititori în aşteptarea)
Monitorul, care urmează traduce direct aceste condiţii.
fich : monitor;
var scr : boolean;
nc : integer;
c_scr, c_cit : condiţie;
procedura debut_citire;
begin
nc:=nc+1;
if scr then
c_cit.aşteptare;
endif
end
procedura terminare_citire;
begin
nc:=nc-1;
if nc=0 then -- ultimul cititor a terminat
c_scr.semnalizare
endif
end
procedura debut_scriere;
begin
if scr or nc>0 then -- scriere sau citire în curs
c_scr.aşteptare
endif;
scr:=true
end
procedura terminare_scriere;
begin

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.12


scr:=false;
if nc>0 then -- prioritate cititorilor care aşteaptă
c_cit.semnalizare
else
c_scr.semnalizare
endif
end
begin -- iniţializare
scr:=false;
nc:=0
end
end fich
Pot fi definite şi programate şi alte reguli de prioritate.
3.4.3. Comunicarea între procese
Procesele pot comunica prin accesarea unei mulţimi de variabile comune. Acest mod de comunicare este slab
structurat şi ineficace, deoarece el trebuie să asigure excluderea reciprocă a variabilelor. Este utilizat doar în cazuri
speciale, cum ar fi un nucleu de sincronizare, unde excluderea mutuală globală este redusă la secvenţe scurte şi bine
protejate. Pentru cazuri generale sunt utilizate alte moduri de comunicare. Vom prezenta mai întâi o schemă de bază
pentru comunicarea prin mesaje - modelul producătorului şi consumatorului, realizat cu ajutorul monitoarelor (3.4.3.1).
O altă posibilitate, descrisă în 3.4.3.2, constă în a considera operaţiile de comunicare ca un fel de mecanisme primitive
de sincronizare. În 3.4.3.3 prezentăm o aplicaţie frecventă de comunicare – modelul client-server.
3.4.3.1. 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);

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.13


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.
3.4.3.2. 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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.14


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ă.
În cazul desemnării directe parametrul origine a primitivei recepţie poate fi interpretat în două moduri:
 fie ca dată: receptorul specifică explicit că aşteaptă un mesaj de la un destinatar special (recepţie selectivă),
 fie ca rezultat: receptorul primeşte un mesaj care i-a fost adresat împreună cu identitatea emiţătorului.
În cazul desemnării indirecte asocierea proceselor cutiilor poştale poate fi statică sau dinamică. În ultimul caz, sunt
utilizate două primitive conectare şi deconectare pentru ataşarea procesului la o cutie poştală (în calitate de receptor) şi
de abrogare a acestei ataşări, respectiv. În unele sisteme un receptor sau mai multe pot fi ataşate unei cutii poştale date;
cutiile poştale supuse unor asemenea restricţii sunt adesea numite porţi. Este posibilă şi situaţia inversă când un proces
poate fi asociat la mai multe porţi distincte. Dacă asocierea între procesul receptor şi poartă este statică, un nume de
poartă specifică fără ambiguitate un proces receptor ca şi în metoda desemnării directe.
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.
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. ◄

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.15


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ă. ◄
3.4.3.3. 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.
3.4.4. 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.
3.4.4.1. 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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.16


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.
3.4.4.2. 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.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.17


3.4.5. 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.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.18


<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.

3.5. 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ă

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.19


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 ◄

3.6. 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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.20


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.
3.6.1. 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.
3.6.2. 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).
3.6.3. 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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.21


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).
3.6.4. 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.
3.6.4.1. 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.
3.6.4.2. 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.
3.6.4.3. 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.
3.6.4.4. 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.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.22


3.6.4.5. 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.
3.6.4.6. 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.
3.6.5. 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.
3.6.6. 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:

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.23


// 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ă.
3.6.6.1. 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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.24


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

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.25


::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.
3.6.6.2. 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);

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.26


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);
}
3.6.6.3. 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:

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.27


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();
}
3.6.6.4. 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():

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.28


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;

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.29


}
ş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();
}

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.30


// 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ă.

3.7. Exerciţii la capitolul 3


Exerciţiul 3.1. Să se exprime în termeni de procese executarea programelor în corutine (v.2.1). Precizaţi contextul şi
modul de sincronizare a acestor procese.
Exerciţiul 3.2. În programele cititorului şi redactorului, prezentate în 3.4.2.2, cititorii pot ocupa resursa permanent,
blocând pentru un timp redactorii.
1) Propuneţi o soluţie cu prioritate pentru redactori în care accesarea resursei ar fi interzisă unor noi cititori, dacă
există un redactor în aşteptare.
2) Soluţia de mai sus prezintă un risc de aşteptare pentru un timp nedefinit a cititorilor. Specificaţi un mod de
funcţionare care ar trata într-o manieră echitabilă cititorii şi redactorii şi elaboraţi programul monitorului
respectiv.
Exerciţiul 3.3. Buticul unui bărbier este organizat conform schemei de mai jos:

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.31


Uşa de intrare şi cea de comunicare dintre sala de aşteptare şi salonul bărbierului nu permit intrarea simultană a mai
multor clienţi (doar unul singur poate trece prin fiecare uşa la un moment de timp dat). Aceste uşi au un mecanism
culisant, care face ca una din ele să fie închisă, atunci când cealaltă este deschisă. Bărbierul invită următorul client,
atunci când termină deservirea clientului curent. Dacă sala de aşteptare este goală el doarme. Dacă un client găseşte
bărbierul dormind, îl trezeşte, altfel îşi aşteaptă rândul în sala de aşteptare.
Reprezentând bărbierul şi clienţii prin procese, programaţi funcţionarea acestui sistem cu ajutorul unui monitor.
Exerciţiul 3.4. O linie de cale ferată, care leagă două oraşe A şi B, conţine o porţiune cu un singur drum. Vom
reprezenta trenurile prin procese conform schemei de mai jos:
trenurile A → B trenurile B → A
... ...
sens_unic.intrare_vest sens_unic.intrare_est
<traversare sens unic> <traversare sens unic>
sens_unic.ieşire_est sens_unic.ieşire_vest
... ...

B
A
Rolul monitorului sens_unic este să garanteze că toate trenurile, angajate la un moment de timp dat pe porţiunea cu
un singur drum, circulă în aceeaşi direcţie.
1) Să se elaboreze programul monitorului sens_unic, presupunând că numărul trenurilor prezente pe porţiunea cu
un singur drum, nu este limitat.
2) Aceeaşi problema, dar pentru un număr limitat N de trenuri.
3) Examinaţi în ambele cazuri riscul de blocare şi mijloacele de evitare ale acestora.
Exerciţiul 3.5. Cinci filozofi stau la o masă conform schemei de mai jos.

Filosoful 0

Filosoful 4 Filosoful 1

Filosoful 3 Filosoful 2

Fiecare, schematic prezentat printr-un proces pi, (i=0..4), se comportă conform programului:
ciclu
gândire -- nu necesită vre-o resursă
mâncare -- sunt necesare două (ambele) beţişoare (pentru orez)
endciclu
Fiecare filozof poate utiliza doar cele două furculiţe, plasate de o parte şi alta a farfuriei sale, la terminare
întorcându-le în poziţia iniţială. Durata celor două faze este arbitrară, dar finită.
1) Să se arate, că alocarea furculiţelor poate conduce la blocarea sistemului (blocarea a două sau mai multe
procese).

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.32


2) Să se elaboreze un monitor furculiţe, care ar realiza alocarea furculiţelor fără blocare. Apelarea procedurii
mâncare va fi înlocuită în programul procesului pi prin secvenţa
furculiţe.luare(i)
mâncare
furculiţe.eliberare(i)
Apelarea procedurii luare îl blochează pe apelant pentru perioada de timp în care cele două furculiţe adiacente
farfuriei sale sunt utilizate de către vecinii săi. Apelarea procedurii eliberare eliberează două furculiţe.
3) Examinaţi posibilitatea de blocare indefinită a unui proces şi propuneţi soluţii pentru a o evita.
Rezolvarea punctului 1. Blocare reciprocă
Filozoful i
gândire();
P(furculiţa i);
P(furculiţa (i+1) mod 5);
mâncare();
V(furculiţa i);
V(furculiţa (i+1) mod 5);
Dacă fiecare filozof va lua în acelaşi moment de timp furculiţa din partea sa dreaptă va avea loc o blocare reciprocă.
Rezolvarea punctului 2.
Filozoful i luare_furculiţă(i) întoarcer_furculiţă(i)
gândire(); P(mutex); P(mutex);
luare_furculiţă(i); stare[i] = FOAME; stare[i] = GÂNDIRE;
mâncare(); test(i); test(STÂNGA);
întoarcere_furculiţă(i); V(mutex); test(DREAPTA);
P(s[i]); V(mutex);
test(i)
if(stare[i] = FOAME && stare[DREAPTA] != MÂNCARE && stare[STÂNGA] != MÂNCARE) then
stare[i] = MÂNCARE;
V(s[i]);
Exerciţiul 3.6. Specificaţi şi elaboraţi cu ajutorul unui monitor două primitive emitere şi recepţie, care realizează
comunicarea între două procese via un tampon de capacitatea unui singur mesaj cu sincronizare prin metoda rendez-
vous.
Exerciţiul 3.7. Programul de mai jos reprezintă două procese, care funcţionează conform schemei producător-
consumator şi sincronizate prin aşteptare activă.
producător consumator
ciclu ciclu
test_c: if n=0 then
generare(mesaj);
test_p: if n=N then go to test_c
go to test_p endif;
endif; n:=n-1;
n:=n+1; ieşire(mesaj);
intrare(mesaj); consumare(mesaj)
endciclu endciclu
Procedurile intrare şi ieşire sunt cele din 3.4.3.1.
Să se discute validitatea acestor programe şi să se aducă, eventual, modificări necesare, presupunând:
1) că operaţiile de consultare, incrementare şi decrementare a unei variabile în memorie sunt indivizibile,
2) că indivizibile sunt doar încărcarea, ordonarea, incrementare şi decrementarea unui registru; operaţiile de
comparare, incrementare şi decrementare a unei variabile necesită încărcarea sa într-un registru.
Exerciţiul 3.8. Programul de mai jos reprezintă două procese, care funcţionează conform schemei producător-
consumator şi sincronizate prin aşteptare activă.
producător consumator
ciclu ciclu

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.33


generare(mesaj); test_c: if NC-NP0 then
test_p: if NP-NC  N then go to test_c
go to test_p endif;
endif; ieşire(mesaj);
intrare(mesaj); consumare(mesaj)
NP:=NP+1; NC:=NC+1
endciclu endciclu
Să se discute validitatea acestor programe. Ce reprezintă variabilele întregi NP şi NC şi care sunt valorile lor
iniţiale?
Exerciţiul 3.9. Un sistem constă din n cupluri de procese comunicante două câte două conform schemei producător-
consumator. Tampoanele sunt de lungime variabilă şi administrate dinamic. Atunci când un producător are nevoie de o
casetă pentru a depozita un mesaj, el cere aceasta unei rezerve de casete, o umple şi o pune în firul de aşteptare al
tamponului său. Când un consunator a preluat un mesaj dintr-o casetă, el restituie caseta rezervei de casete. Iniţial
rezerva conţine N casete, fiecare fiind identificată printr-o adresă adr[0..N-1]. Elaboraţi monitorul gestionării rezervei
de casete în următoarele două ipoteze:
1) Toate cuplurile de procese sunt tratate identic.
2) Prioritatea cuplului este cu atât mai mare cu cât mai puţine tampoane cuplul ocupă (această politică vizează
restabilirea echităţii pentru cazul unor procese care derulează cu viteze diferite).
Exerciţiul 3.10. Să se programeze cu ajutorul monitoarelor problema de culegere a rezultatelor de măsurare propus
în exerciţiul 2.2.
Exerciţiul 3.11. Modificaţi monitorul gestionării unui periferic asociindu-i un monitor de gardă, astfel ca tratarea
unei erori să fie declanşată, dacă transferul nu a fost terminat după un interval dat de timp.
Exerciţiul 3.12. Pentru realizarea unui server de imprimare se cere să se utilizeze metoda de buferizare, descrisă în
3.4.4. Un client se poate adresa acestui server printr-o comandă imprimare(f), în care f este un nume de fişier.
Presupunem că serverul dispune de informaţiile necesare citirii unei serii de linii, care formează fişierul f.
1) Elaboraţi programul serverului imprimantei.
2) Modificaţi acest program astfel încât clientului să i se permită:
 anularea unei imprimări cerute, înainte ca ea să fi avut loc,
 prevenirea clientului despre terminarea imprimării fişierului prin depozitarea unui mesaj într-o cutie
poştală specificată.
3) Modificaţi programul serverului pentru a permite mai multor imprimante să funcţioneze în paralel.
Exerciţiul 3.13. Vrem să construim un sistem de tratare a lucrărilor organizate pe loturi, analogic exerciţiului 9.15.
Lucrările sunt citite secvenţial de un cititor de cartele, rezultatele sunt listate la o imprimantă.
Sistemul este construit din trei procese: citire, care comandă cititorul, execuţie, care realizează execuţia unei lucrări
şi imprimare, care comandă imprimarea. Procesul imprimare este de forma:
ciclu
<citire cartelă>
<tratare cartelă>
endciclu
programul tratare cartelă poate conţine apelări <imprimare linie>.
1) Presupunem mai întâi că toate intrările-ieşirile sunt tamponate în memoria centrală. Realizaţi cu ajutorul
monitoarelor cooperarea celor trei procese.
2) Modificaţi programul precedent pentru a putea trata cazul erorilor în cursul execuţiei (presupunem, că procesul
citire poate detecta prima cartelă a unei lucrări).
3) Modificaţi programul precedent pentru a integra aici administrarea consolei operatorului (cu specificaţiile
exerciţiului 9.15).
4) Modificaţi programele precedente pentru a introduce administrarea tamponată a intrărilor şi ieşirilor (exerciţiul
precedent).

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.34


Exerciţiul 3.14. Clienţii unui server sunt împărţiţi în două clase, numite 1 şi 2. Cererile clienţilor din clasa 1 sunt
prioritare: serverul va trate o cerere din clasa 2 doar în lipsa de cereri de clasa 1.Cererile din aceeaşi clasa vor fi tratate
în ordinea sosirii. Un câmp al cererii identifică prioritatea.
1) Programaţi sincronizarea clienţilor şi a serverului utilizând monitoare. Să se folosească un fir de aşteptare
pentru fiecare clasă de cereri şi un proces dispecer pentru alimentarea acestor fire.
2) Schema de mai sus conţine un risc de blocare. Modificaţi specificaţiile şi realizarea pentru a elimina acest risc,
păstrând tratarea privilegiată a cererilor din clasa 1.

16.01.19 19:27 C:\Users\R0tleSS\Desktop\C3_DL.doc p.35

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