Laboratoare:Sincronizare thread-uri

= Fire de execuție (II) - Mecanisme de sincronizare =

Pentru sincronizarea firelor de execuție avem la dispoziție:
 * secțiuni critice (excludere mutuală în cadrul aceluiași proces) – doar Win32
 * mutex: POSIX, Win32
 * semafoare: POSIX, Win32
 * variabile de condiție: POSIX, Win32 (începând cu Vista)
 * evenimente: doar Win32
 * timere: doar Win32.

Standardul POSIX specifică funcții de sincronizare pentru fiecare tip de obiect de sincronizare. API-ul Win32, fiind controlat de o singură entitate, permite ca toate obiectele de sincronizare să poată fi utilizate cu funcțiile standard de sincronizare: WaitForSingleObject, WaitForMultipleObjects sau SignalObjectAndWait.

Mutex
Mutexurile sunt obiecte de sincronizare utilizate pentru a asigura accesul exclusiv la o secțiune de cod în care se accesează date partajate între două sau mai multe fire de execuție. Un mutex are două stări posibile: ocupat și liber. Un mutex poate fi ocupat de un singur fir de execuție la un moment dat. Atunci când un mutex este ocupat de un fir de execuție, el nu mai poate fi ocupat de niciun altul. În acest caz, o cerere de ocupare venită din partea unui alt fir, în general va bloca firul până în momentul în care mutexul devine liber.

Inițializarea/distrugerea unui mutex
Un mutex poate fi inițializat/distrus în mai multe moduri:
 * folosind o macrodefiniție
 * inițializat cu atribute implicite
 * inițializare cu atribute explicite

NB: Mutexul trebuie să fie liber pentru a putea fi distrus. În caz contrar funcția va întoarce codul de eroare EBUSY. Întoarcerea valorii 0 semnifică succesul apelului.

Tipuri de mutexuri
Folosind atributele de inițializare se pot crea mutexuri cu proprietăți speciale:
 * activarea moștenirii de prioritate (priority inharitance) pentru a preveni inversiunea de prioritate (priority invesion). Există trei protocoale de moștenire a priorității:
 * PTHREAD_PRIO_NONE – nu se moștenește prioritatea când deținem mutexul creat cu acest atribut
 * PTHREAD_PRIO_INHERIT – dacă deținem un mutex creat cu acest atribut și dacă există fire de execuție blocate pe acel mutex se moștenește prioritatea firului de execuție cu cea mai mare prioritate
 * PTHREAD_PRIO_PROTECT – dacă firul de execuție curent deține unul sau mai multe mutexuri, acesta va executa la maximul priorităților specificată pentru toți mutecșii deținuți.
 * modul de comportare la preluări recursive ale mutexului
 * PTHREAD_MUTEX_NORMAL – nu se fac verificări, preluarea recursivă duce la deadlock
 * PTHREAD_MUTEX_ERRORCHECK – se fac verificări, preluarea recursivă duce la întoarcerea unei erori
 * PTHREAD_MUTEX_RECURSIVE – mutexurile pot fi preluate recursiv, și trebuie eliberate de același număr de ori.

Ocuparea/eliberearea unui mutex
Funcțiile de ocupare blocantă/eliberare a unui mutex: Dacă mutexul este liber în momentul apelului, acesta va fi ocupat de firul apelant și funcția va întoarce imediat. Dacă mutexul este ocupat de un alt fir, apelul va bloca până la eliberarea mutexului. Dacă mutexul este deja ocupat de firul de execuție curent (lock recursiv), comportamentul funcției este dictat de tipul mutexului:

Nu este garantată o ordine FIFO de ocupare a unui mutex. Oricare din firele aflate în așteptare la deblocarea unui mutex pot să-l acapareze.

Încercarea neblocantă de ocupare a unui mutex
Pentru a încerca ocuparea unui mutex fără a aștepta eliberarea acestuia în cazul în care este deja ocupat, se va apela funcția:

Exemplu de utilizare a mutexurilor
Un exemplu de utilizare a unui mutex pentru a serializa accesul la variabila globală global_counter:

Futexuri
Mutexurile din firele de execuție POSIX sunt implementate cu ajutorul futexurilor, din considerente de performanță. Numele de futex vine de la Fast User-space muTEX. Ideea de la care a plecat implementarea futexurilor a fost aceea de a optimiza operația de ocupare a unui mutex în cazul în care acesta nu este deja ocupat. Dacă mutexul nu este ocupat, el va fi ocupat fără ca procesul care îl ocupă să se blocheze. În acest caz, nefiind necesară blocarea, nu este necesar ca procesul să intre în kernel-mode (pentru a intra într-o stare de așteptare). Optimizarea constă în testarea și setarea atomică a valorii mutexului (printr-o instrucțiune de tip test-and-set-lock) în user-space, eliminându-se trap-ul în kernel în cazul în care nu este necesară blocarea.

Futexul poate fi orice variabilă dintr-o zonă de memorie partajată între mai multe fire de execuție sau procese. Așadar, operațiile efective cu futexurile se fac prin intermediul funcției do_futex, disponibilă prin includerea headerului linux/futex.h. Signatura ei arată astfel: În cazul în care este necesară blocarea, do_futex va face un apel de sistem - sys_futex. Futexurile pot fi utile (și poate fi necesară utilizarea lor explicită) în cazul sincronizării proceselor, alocate în variabile din zone de memorie partajată între procesele respective.

Semafor
Semafoarele sunt obiecte de sincronizare ce reprezintă o generalizare a mutexurilor prin aceea că salvează numărul de operații de eliberare (incrementare) efectuate asupra lor. Practic, un semafor reprezintă un întreg care se incrementează/decrementează atomic. Valoarea unui semafor nu poate scădea sub 0. Dacă semaforul are valoarea 0, operația de decrementare se va bloca până când valoarea semaforului devine strict pozitivă. Mutexurile pot fi privite, așadar, ca niște semafoare binare.

Operațiile care pot fi efectuate asupra semafoarelor POSIX sunt:

Semafoarele POSIX au fost prezentate în cadrul laboratorului de comunicare inter-proces.

Variabila de condiție
Variabilele condiție pun la dispoziție un sistem de notificare pentru fire de execuție, permițându-i unui fir să se blocheze în așteptarea unui semnal din partea unui alt fir. Folosirea corectă a variabilelor condiție presupune un protocol cooperativ între firele de execuție.

Mutexurile (mutual exclusion locks) și semafoarele permit blocarea altor fire de execuție. Variabilele de condiție se folosesc pentru a bloca firul curent de execuție până la îndeplinirea unei condiții.

Variabilele condiție sunt obiecte de sincronizare care-i permit unui fir de execuție să-și suspende execuția până când o condiție (predicat logic) devine adevărată. Când un fir de execuție determină că predicatul a devenit adevărat, va semnala variabila condiție, deblocând astfel unul sau toate firele de execuție blocate la acea variabilă condiție (în funcție de cum se dorește).

O variabilă condiție trebuie întotdeauna folosită împreună cu un mutex pentru evitarea race-ului care se produce când un fir se pregătește să aștepte la variabila condiție în urma evaluării predicatului logic, iar alt fir semnalizează variabila condiție chiar înainte ca primul fir să se blocheze, pierzându-se astfel semnalul. Așadar, operațiile de semnalizare, testare a condiției logice și blocare la variabila condiție trebuie efectuate având ocupat mutexul asociat variabilei condiție. Condiția logică este testată sub protecția mutexului, iar dacă nu este îndeplinită, firul apelant se blochează la variabila condiție, eliberând atomic mutexul. În momentul deblocării, un fir de execuție va încerca să ocupe mutexul asociat variabilei condiție. De asemenea, testarea predicatului logic trebuie făcută într-o buclă, pentru că dacă sunt eliberate mai multe fire deodată, doar unul va reuși să ocupe mutexul asociat condiției. Restul vor aștepta ca acesta să-l elibereze, însă este posibil ca firul care a ocupat mutexul să schimbe valoarea predicatului logic pe durata deținerii mutexului. Din acest motiv celelalte fire trebuie să testeze din nou predicatul pentru că altfel și-ar începe execuția presupunând predicatul adevărat, când el este, de fapt, fals.

Inițializarea/distrugerea unei variabile de condiție
Ca și la mutexuri:
 * dacă parametrul attr este nul se folosesc atribute implicite
 * trebuie să nu existe nici un fir de execuție în așteptare pe variabila de condiție atunci când aceasta este distrusă, altfel se întoarce EBUSY</tt>.

Blocarea la o variabilă condiție
Pentru a-și suspenda execuția și a aștepta la o variabilă condiție, un fir de execuție va apela: Firul de execuție apelant trebuie să fi ocupat deja mutexul asociat, în momentul apelului. Funcția pthread_cond_wait va elibera mutexul și se va bloca, așteptând ca variabila condiție să fie semnalizată de un alt fir de execuție. Cele două operații sunt efectuate atomic. În momentul în care variabila condiție este semnalizată, se va încerca ocuparea mutexului asociat, și după ocuparea acestuia, apelul funcției va întoarce. Observați că firul de execuție apelant poate fi suspendat, după deblocare, în așteptarea ocupării mutexului asociat, timp în care predicatul logic, adevărat în momentul deblocării firului, poate fi modificat de alte fire. De aceea, apelul pthread_cond_wait trebuie efectuat într-o buclă în care se testează valoarea de adevăr a predicatului logic asociat variabilei condiție, pentru a asigura o serializare corectă a firelor de execuție. Un alt argument pentru testarea în buclă a predicatului logic este acela că un apel pthread_cond_wait poate fi întrerupt de un semnal asincron (vezi laboratorul de semnale), înainte ca predicatul logic să devină adevărat. Dacă firele de execuție care așteptau la variabila condiție nu ar testa din nou predicatul logic, și-ar continua execuția presupunând greșit că acesta e adevărat.

Blocarea la o variabilă condiție cu timeout
Pentru a-și suspenda execuția și a aștepta la o variabilă condiție, nu mai târziu de un moment specificat de timp, un fir de execuție va apela: Funcția se comportă la fel ca pthread_cond_wait, cu excepția faptului că dacă variabila condiție nu este semnalizată mai devreme de abstime, firul apelant este deblocat, și după ocuparea mutexului asociat, funcția se întoarce cu eroarea ETIMEDOUT. Parametrul abstime este absolut și reprezintă numărul de secunde trecute de la 1 ianuarie 1970, ora 00:00.

Deblocarea unui singur fir blocat la o variabilă condiție
Pentru a debloca un singur fir de execuție blocat la o variabilă condiție se va semnaliza variabila condiție astfel: Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea se va pierde. Dacă la variabila condiție așteaptă mai multe fire de execuție, va fi deblocat doar unul dintre acestea. Alegerea firului care va fi deblocat este făcută de planificatorul de fire de execuție. Nu se poate presupune că firele care așteaptă vor fi deblocate în ordinea în care și-au început așteptarea. Firul de execuție apelant trebuie să dețină mutexul asociat variabilei condiție în momentul apelului acestei funcții.

Exemplu:

Deblocarea tuturor firelor blocate la o variabilă condiție
Pentru a debloca toate firele de execuție blocate la o variabilă condiție, se semnalizează variabila condiție astfel: Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea se va pierde. Dacă la variabila condiție așteaptă fire de execuție, toate acestea vor fi deblocate, dar vor concura pentru ocuparea mutexului asociat variabilei condiție. Firul de execuție apelant trebuie să dețină mutexul asociat variabilei condiție în momentul apelului acestei funcții.

Exemplu de utilizare a variabilelor de condiție
În următorul program se utilizează o barieră pentru a sincroniza firele de execuție ale programului. Bariera este implementată cu ajutorului unei variabile de condiție.

Din execuția programului se observă:
 * ordinea în care sunt planificate firele de execuție nu este numaidecât cea a creării lor
 * ordinea în care sunt trezite firele de execuție ce așteaptă la o variabilă de condiție nu este numaidecât ordinea în care acestea au intrat în așteptare.

Bariera
Standardul POSIX definește și un set de funcții și structuri de date de lucru cu bariere. Aceste funcții sunt disponibile dacă se definește macro-ul _XOPEN_SOURCE</tt> la o valoare >= 600.

Cu bariere POSIX, programul de mai sus poate fi simplificat:

Așteptarea la o barieră
Dacă bariera a fost creată cu count=N</tt>, primele N-1</tt> fire de execuție care apelează pthread_barrier_wait</tt> se blochează. Când sosește ultimul (al N</tt>-lea), va debloca toate cele N-1</tt> fire de execuție. Funcția pthread_barrier_wait</tt> întoarce trei valori:
 * EINVAL</tt> – în cazul în care bariera nu este inițializată (singura eroare definită)
 * PTHREAD_BARRIER_SERIAL_THREAD</tt> – în caz de succes, un singur fir de execuție va întoarce valoarea aceasta – nu e specificat care este acel fir de execuție (nu e obligatoriu să fie ultimul ajuns la barieră)
 * 0</tt> – valoare întoarsă în caz de succes de celelalte N-1</tt> fire de execuție.

Mecanisme de sincronizare a firelor de execuție Windows
Pentru sincronizarea firelor de execuție Windows sunt disponibile o serie de obiecte de sincronizare la care firele de execuție pot aștepta prin apelarea uneia din funcțiile de așteptare (WaitForSingleObject, WaitForMultipleObjects ori SignalObjectAndWait).

Obiectele de sincronizare Semaphore, Mutex, Event și WaitableTimer pot fi folosite atât pentru sincronizarea proceselor cât și a firelor de execuție. Ele au fost deja introduse în laboratoarele trecute.

În Windows mai există un mecanism de sincronizare care este disponibil doar pentru firele de execuție ale aceluiași proces, și anume CriticalSection. Se recomandă folosirea CriticalSection pentru excluderea mutuală a firelor de execuție ale aceluiași proces, fiind mai eficient decât Mutex sau Semaphore.

Win32 API pune la dispoziție un mecanism de acces sincronizat la variabile partajate între fire de execuție prin intermediul funcțiilor interlocked (Interlocked Variable Access), precum și operații atomice de inserare și ștergere în liste simplu înlănțuite (Interlocked Singly Linked Lists).

Mutex Win32
Subiectul a fost tratat în laboratorul de comunicație inter-proces

Semafor Win32
Subiectul a fost tratat în laboratorul de comunicație inter-proces

Secțiune critică
Obiectele CriticalSection sunt echivalente mutexurilor POSIX de tip RECURSIVE. Obiectele CriticalSection sunt folosite pentru excluderea mutuală a accesului firelor de execuție ale aceluiași proces la o secțiune critică de cod care conține operații asupra unor date partajate. Un singur fir de execuție va fi activ la un moment dat în interiorul secțiunii critice, și dacă mai multe fire așteaptă să intre, nu este garantată ordinea lor de intrare, totuși sistemul va fi echitabil față de toate.

Operațiile care se pot efectua asupra unei secțiuni critice sunt: intrarea, intrarea neblocantă, ieșirea din secțiunea critică, inițializarea și distrugerea.

Pentru serializarea accesului la o secțiune de cod critică, fiecare fir de execuție va trebui să intre într-un obiect CriticalSection la începutul secțiunii și să-l părăsească la sfârșitul ei. În acest fel, dacă două fire de execuție încearcă să intre în CriticalSection simultan, doar unul dintre ele va reuși, și își va continua execuția în interiorul secțiunii critice, iar celălalt se va bloca pînă când obiectul CriticalSection va fi părăsit de primul fir. Așadar, la sfârșitul secțiunii, primul fir trebuie să părăsească obiectul CriticalSection, permițându-i celuilalt intrarea.

Pentru excluderea mutuală se pot folosi atât obiecte Mutex, cât și obiecte CriticalSection; dacă sincronizarea trebuie făcută doar între firele de execuție ale aceluiași proces este recomandată folosirea CriticalSection, fiind mai un mecanism mai eficient. Operația de intrare în CriticalSection se traduce într-o singură instrucțiune de asamblare de tip test-and-set-lock (TSL). CriticalSection este echivalentul futexului din Linux.

Inițializarea/distrugerea unei secțiuni critice
Alocarea memoriei pentru o secțiune critică se face prin declararea unui obiect CRITICAL_SECTION</tt>. Acesta nu va putea fi folosit, totuși, înainte de a fi inițializat.

Un obiect CRITICAL_SECTION</tt> nu poate fi copiat ori modificat după inițializare. De asemenea, un obiect CRITICAL_SECTION</tt> nu trebuie inițializat de două ori, în caz contrar, comportamentul său fiind nedefinit.

Contorul de spin are sens doar pe sistemele multiprocesor (SMP) (este ignorat pe sisteme uniprocesor). Contorul de spin reprezintă numărul de cicli pe care îi petrece un fir de execuție pe un procesor în busy-waiting, înainte de a-și suspenda execuția la un semafor asociat secțiunii critice în așteptarea eliberării acesteia. Scopul așteptării unui număr de cicli în busy-waiting este evitarea blocării la semafor în cazul în care secțiunea critică se eliberează în intervalul respectiv, deoarece blocarea la semafor are impact asupra performanțelor. Folosirea contorului de spin este recomandată mai ales în cazul unei secțiuni critice scurte, accesate foarte des.

Utilizarea secțiunilor critice
Secțiunile critice Windows au comportamentul mutexurilor POSIX de tip RECURSIVE. Un fir de execuție care se află deja în secțiunea critică nu se va bloca dacă apelează din nou EnterCriticalSection, însă va trebui să părăsească secțiunea critică de un număr de ori egal cu cel al ocupărilor, pentru a o elibera.

În cadrul unui fir de execuție, numărul apelurilor LeaveCriticalSection</tt> trebuie să fie egal cu numărul apelurilor <tt>EnterCriticalSection</tt>, pentru a elibera în final secțiunea critică. Dacă un fir de execuție care nu a intrat în secțiunea critică apelează <tt>LeaveCriticalSection</tt>, se va produce o eroare care va face ca firele care au apelat <tt>EnterCriticalSection</tt> să aștepte pentru o perioadă nedefinită de timp.

Operații atomice cu variabile partajate (Interlocked Variable Access)
Funcțiile <tt>interlocked</tt> pun la dispoziție un mecanism de sincronizare a accesului la variabile partajate între mai multe fire de execuție. Funcțiile pot fi apelate de fire de execuție ale unor procese diferite, pentru variabile aflate într-un spațiu de memorie partajată. Funcțiile <tt>interlocked</tt> reprezintă cel mai simplu mod de evitare a race-ului care apare când două fire de execuție modifică aceeași variabilă.

Operațiile atomice asupra variabilelor partajate:
 * incrementare/decrementare
 * atribuirea atomică a unei valori unei variabile partajate.


 * atribuirea atomică după o condiție a unei valori variabilei partajate

<tt>InterlockedCompareExchange</tt> va compara <tt>dest</tt> cu <tt>comp</tt>; dacă sunt egale îi va atribui lui <tt>dest</tt> valoarea <tt>exchange</tt>. Testul și atribuirea vor fi executate într-o singură operație atomică. Pentru variabile de tip pointer se va folosi <tt>InterlockedCompareExchangePointer</tt>. Comportamentul este echivalent cu:

Windows Thread Pooling
Pentru a facilita dezvoltarea de aplicații eficiente bazate pe fire de execuție, sistemul de operare Windows pune la dispoziție mecanismul thread pooling. Utilizarea thread pooling este benefică în cazul unei aplicații bazată pe fire de execuție care au de îndeplinit taskuri relativ scurte. Prin utilizarea thread pooling, fiecare task de efectuat va fi atribuit unui fir de execuție din pool, eliminându-se calculele suplimentare impuse de crearea și terminarea unui fir de execuție. Prin task se înțelege o procedură executată de un fir de execuție din thread pool.

Există două modalități prin care o aplicație poate specifica taskurile care dorește să fie executate de fire de execuție din thread pool.
 * se pot adăuga taskuri ce vor fi executate imediat ce se eliberează un fir de execuție din thread pool
 * se pot adăuga operații de așteptare care au asociată o funcție callback ce urmează a fi executată la sfârșitul unui timeout de unul din firele de execuție din thread pool. Din această categorie fac parte operațiile de așteptare a terminării unei intrări/ieșiri asincrone, operațiile de așteptare a expirării unui TimerQueue Timer și funcțiile de așteptare înregistrate.

Dacă vreuna din funcțiile executate într-un thread-pool apelează <tt>TerminateThread</tt> comportamentul nu este definit.

Așteptarea unei operații de intrare/ieșire asincrone
Pentru a adăuga la thread pool un task care se va executa la finalul unei operații de intrare/ieșire asincrone pe un anumit FileHandle, se va apela funcția:

Adăugarea unui task pentru execuție imediată
Pentru a adăuga la thread pool un task care să fie executat imediat se va apela funcția:

Timer Queues
Obiectele <tt>TimerQueue</tt> reprezintă cozi de timere. Ele conțin obiecte <tt>TimerQueueTimer</tt> care au asociată o funcție <tt>callback</tt> ce va fi executată de un fir de execuție din <tt>thread pool</tt> la expirarea timerului.

Crearea unui timer
Pentru crearea unui timer se va apela funcția:

Registered Wait Functions
Funcțiile de așteptare înregistrate sunt funcții de așteptare executate de un fir de execuție din thread pool. În momentul în care obiectul de sincronizare după care se așteaptă trece în starea signaled, se va executa rutina callback asociată funcției de așteptare înregistrate, de un fir de execuție din thread pool. În mod implicit, funcțiile de așteptare înregistrate se rearmează automat și rutinele callback sunt executate de fiecare dată când obiectul de sincronizare după care se așteaptă trece în starea signaled, sau intervalul de timeout expiră. Acest lucru se repetă până când înregistrarea funcției de așteptare este anulată. Se poate seta, însă, ca funcția de așteptare înregistrată să se execute o singură dată.

Înregistrarea unei funcții de așteptare
Pentru înregistrarea în thread pool a unei funcții de așteptare se va apela funcția: De fiecare dată când hObject trece în starea signaled, și la fiecare dwMilliseconds, rutina Callback va fi executată cu parametrul Context de un fir de execuție din thread pool. Rutina Callback trebuie să nu apeleze TerminateThread și să aibă următoarea signatură: Parametrul TimerOrWaitFired va specifica dacă execuția rutinei Callback s-a declanșat în urma trecerii în starea signaled a obiectului de sincronizare, sau în urma expirării intervalului de timeout specificat.

Prin intermediul parametrului dwFlags se pot transmite caracteristici ale firului de execuție care va executa rutina Callback, precum și dacă funcția de așteptare trebuie să se execute doar o singură dată. Funcția va întoarce, prin parametrul phNewWaitObject, un handle ce va fi folosit pentru deînregistrarea funcției de așteptare.

Deînregistrarea unei funcții de așteptare
Pentru a anula înregistrarea unei funcții de așteptare se va apela una din funcțiile: Orice funcție de așteptare înregistrată va trebui deînregistrată prin apelul uneia din funcțiile de mai sus.

Funcția UnregisterWaitEx va semnaliza eventul CompletionEvent în cazul în care se termină cu succes și rutina de callback s-a terminat cu succes. Dacă valoarea lui CompletionEvent nu este NULL, atunci funcția va aștepta finalizarea operației de așteptare și terminarea rutinei asociate.

Quiz
Pentru autoevaluare raspundeți la întrebările din acest quiz.

Exercitii pre-laborator
<ol> <li> Se da urmatorul program: <ul> <li>Obtineti fisierul in limbaj de asamblare corespunzator. ( folositi optiunea -S la compilarea cu gcc ).</li> <li>Identificati secventa de cod care realizeaza incrementarea.</li> <li>Cate operatii in limbaj de asamblare sunt necesare? </li></ul></li> <li> Care este diferenta intre un deadlock si un livelock? </li> </ol>

Prezentare
Pentru a urmări mai ușor noțiunile expuse la începutul laboratorului folosiți această prezentare (pdf) (odp).

Exerciții
Arhiva de sarcini a laboratorului.

Linux

 * 1) 1 punct – <tt>unsafe</tt>
 * 2) * Creați <tt>nr=SINGLE_INC_THREADS_NO</tt> fire de execuție care apelează funcția <tt>single_increment</tt> și <tt>nr=DOUBLE_INC_THREADS_NO</tt> fire de execuție care apelează funcția <tt>double_increment</tt>.
 * 3) * Este necesară protejarea variabilei <tt>a</tt> dacă funcția <tt>single_increment</tt> este rulată din mai multe fire de execuție? Dar variabila <tt>b</tt>?
 * 4) * Corectați (dacă este cazul) funcțiile <tt>single_increment</tt> și <tt>double_increment</tt> folosind mutecși POSIX.
 * 5) * Hints
 * 6) ** Folosiți <tt>make test</tt> sau <tt>./test.sh</tt> pentru a testa programul.
 * 7)  1 punct – <tt>blocked</tt>
 * 8) * Inspectați fișierul <tt>blocked.c</tt>, compilați și executați binarul (repetați până detectați blocarea programului). Programul crează două fire de execuție care caută un număr magic, fiecare în intervalul propriu.
 * 9) * Fiecare fir de execuție, pentru fiecare valoare din intervalul propriu verifică dacă este valoarea căutată:
 * 10) ** dacă da, marchează un câmp <tt>found</tt> pentru a înștiința și celălalt thread că a găsit numărul căutat,
 * 11) ** dacă nu, verifică câmpul <tt>found</tt> al structurii celuilalt fir de execuție pentru a vedea dacă acesta a găsit deja numărul căutat.
 * 12) * Determinați cauza blocării și reparați programul și explicați soluția.
 * 13) * HINT: puteți utiliza <tt>helgrind</tt> unul din toolurile <tt>valgrind</tt> pentru a detecta problema: <tt>valgrind --tool=helgrind ./blocked</tt>.
 * 14) 2.5 puncte – <tt>h2o</tt>
 * 15) * Formula de generare a apei este <tt>2H + O = H20</tt>. Atomii sunt reprezentați de fire de execuție.
 * 16) * Creați <tt>H_THREADS</tt> fire de execuție care apelează funcția <tt>hReady</tt>, și <tt>O_THREADS</tt> fire de execuție care apelează funcția <tt>oReady</tt>.
 * 17) * Un <tt>H</tt> nu se combina (thread-ul corespunzător lui nu se termină) până când nu există un alt <tt>H</tt> și un <tt>O</tt>. De asemenea, un <tt>O</tt>, pentru a se combina are nevoie de doi <tt>H</tt>.
 * 18) * Completați funcțiile <tt>hReady</tt> și <tt>oReady</tt> astfel încât să obțineți apă.
 * 19) * HINTS: utilizați semafoare.
 * 20)  2.5 puncte – <tt>prodcons</tt>
 * 21) * Rezolvați problema producător-consumator folosind variabile de condiție și mutecși. Producătorul va produce un anumit număr de întregi pe care îi va pune într-un buffer de lungime limitată de unde consumatorul Îi va lua.
 * 22) * Completați funcțiile <tt>consumer_fn</tt> și <tt>producer_fn</tt>.
 * 23) * HINTS
 * 24) ** Folosiți tipul <tt>buffer_t</tt> pentru a reprezenta buffer-ul. Câmpul <tt>count</tt> reprezintă poziția curentă de inserat în buffer.
 * 25) ** Folosiți funcțiile <tt>init_buffer</tt>, <tt>insert_item</tt>, <tt>remove_item</tt>, <tt>is_buffer_full</tt> și <tt>is_buffer_empty</tt> pentru a lucra cu buffer-ul.

Windows

 * 1) 1 punct – <tt>unsafe</tt>
 * 2) * Creați <tt>nr=SINGLE_INC_THREADS_NO</tt> fire de execuție care apelează funcția <tt>single_increment</tt> și <tt>nr=DOUBLE_INC_THREADS_NO</tt> fire de execuție care apelează funcția <tt>double_increment</tt>.
 * 3) * Este necesară protejarea variabilei <tt>a</tt> dacă funcția <tt>single_increment</tt> este rulată din mai multe fire de execuție? Dar variabila <tt>b</tt>?
 * 4) * Corectați (dacă este cazul) funcțiile <tt>single_increment</tt> și <tt>double_increment</tt> folosind secțiuni critice.
 * 5) 1 puncte – <tt>interlocked</tt>
 * 6) * Creați <tt>THREAD_NO</tt> fire de execuție care incrementează circular o variabilă (când se ajunge la o limită se resetează la 0).
 * 7) * Folosiți <tt>Interlocked Variables</tt> deoarece mecanismul e mai rapid decât o incrementare normală protejată cu <tt>Mutex</tt> sau <tt>CRITICAL_SECTION</tt>.
 * 8) * Incrementarea circulară se va face în funcția <tt>threadfn</tt>.
 * 9) * HINT: Folosiți <tt>InterlockedCompareExchange</tt>
 * 10) 2 puncte – <tt>timerQueue</tt>
 * 11) * Creați un <tt>TimerQueueTimer</tt> a cărui rutină callback să fie declanșată de exact 3 ori, din secundă în secundă. După 3 declanșări se va dezactiva timerul și se vor distruge toate resursele create.
 * 12) * HINT: Trebuie să sincronizați rutina timer-ului cu rutina care îl dezactivează; pentru aceasta puteți folosi un semafor.
 * 13) 2 puncte – <tt>registeredWait</tt>
 * 14) * Rezolvați problema precedentă folosind funcții de așteptare înregistrate. Mai exact rutina callback a funcției de așteptare să se declanșeze de exact 3 ori din secundă în secundă.
 * 15) * HINT: Trebuie să sincronizați funcția înregistrată cu rutina care o dezînregistrează; pentru aceasta puteți folosi un semafor.

Soluții
Soluţii exerciţii laborator 9