Teme:Monitor generic

Termen de predare: Miercuri, 20 mai 2009, ora 23:59:59 (2009-05-20)

Cerința 1: Monitor generic
Să se implementeze un monitor generic, care să poată fi folosit de către mai multe thread-uri pentru rezolvarea unor probleme de sincronizare. Monitorul va fi generic în sensul că va conține un număr de variabile de condiție precizat la crearea sa, cu ajutorul cărora se pot sincroniza thread-urile care îl folosesc.

Implementarea monitorului se va face într-o bibliotecă partajată dinamică pe care o vor încărca thread-urile care se sincronizează cu ajutorul lui. Monitorul va fi expus utilizatorilor săi sub forma unei structuri de date Monitor, care poate fi manipulată cu ajutorul următoarelor primitive (implementate de voi):


 * Monitor* Create(int conditions, char policy) - va crea un monitor cu numărul de variabile condiție și cu politica de funcționare specificate ca parametri
 * int Destroy(Monitor *m) - va distruge monitorul, eliberând toate resursele alocate la crearea acestuia
 * int Enter(Monitor *m) - thread-ul apelant va încerca să intre în monitor
 * int Leave(Monitor *m) - thread-ul apelant va părăsi monitorul
 * int Wait(Monitor *m, int cond) - thread-ul apelant va aștepta după variabila de condiție cond
 * int Signal(Monitor *m, int cond) - thread-ul apelant semnalizează unui alt thread care aștepta după variabila de condiție cond că poate continua execuția; dacă nu există un asemenea thread, apelul nu are nici un efect
 * int Broadcast(Monitor *m, int cond) - thread-ul apelant semnalizează tuturor thread-urilor care așteaptă după variabila de condiție cond că pot continua execuția; dacă nu există un asemenea thread, apelul nu are nici un efect

Toate funcțiile de mai sus întorc 0 în caz de succes și -1 în caz de eroare, cu excepția lui Create. Funcția Create întoarce un pointer la monitor în caz de succes și NULL în caz de eroare.

Trebuie verificate cazurile de folosire incorectă a monitorului precum:
 * Enter</tt> apelat de către un fir de execuție care este deja în monitor
 * Leave, Wait, Signal, Broadcast</tt> - apelat de către un fir de execuție care nu este în monitor
 * Destroy</tt> - apelat când monitorul nu este liber (mai există cel puțin un fir de execuție activ in monitor)

La orice moment de timp, o parte din thread-urile care au apelat Enter</tt> sunt gata să fie planificate spre execuție de către acesta. Thread-urile gata de planificare sunt reținute in 3 cozi:
 * Waiting queue (W) - coada thread-urilor ce au fost trezite de un Signal și doresc să-și continue execuția
 * Signaler queue (S) - coada thread-urilor ce au executat Signal, dar nu au fost încă planificate (este nevidă doar în cazul monitoarelor de tipul SIGNAL_AND_WAIT)
 * Entry queue (E) - coada thread-urilor ce au executat Enter</tt>, dar nu au intrat încă în monitor

În orice moment în care monitorul dorește să planifice un thread spre execuție, el va alege unul din cozile W,S,E, în această ordine (așadar, prioritatea pentru coada W este maximă, iar pentru E este minimă). Modul de alegere al unui thread atunci când sunt mai multe disponibile în aceeași coadă rămâne la latitudinea voastră (implementation-defined).

Politicile de funcționare SIGNAL_AND_CONTINUE și SIGNAL_AND_WAIT sunt singurele valori posibile pentru parametrul policy al funcției Create</tt>. Dacă monitorul funcționează după politica SIGNAL_AND_CONTINUE, un thread care execută Signal</tt> își va continua execuția fără să elibereze monitorul. Dacă monitorul funcționează după politica SIGNAL_AND_WAIT, un thread care execută Signal</tt> cedează monitorul firelor de execuție gata de planificare, el însuși devenind unul dintre ele, fiind mutat în coada S. Implementarea ambelor politici de funcționare este obligatorie.

Codul (binar) executat de thread-urile care folosesc monitorul se împarte în 2 categorii:
 * cod "monitor": codul executat atunci când se apelează una din rutinele Create</tt>, Destroy</tt>, Enter</tt>, Leave</tt>, Wait</tt>, Signal</tt>, Broadcast</tt>, până când acestea se întorc
 * cod "non-monitor": restul codului (codul efectiv util, care folosește sincronizarea oferită de monitor)

O proprietate foarte importantă pe care trebuie să o aibă monitorul implementat de voi este că, la un moment dat de timp, cel mult un thread poate executa cod "non-monitor". Celelalte thread-uri vor aștepta (fie după variabile de condiție, fie în cozile E,S,W, de unde se alege următorul thread pentru planificare) ca cel curent să cedeze monitorul. Numai apoi își vor putea ele executa propriul codul "non-monitor", în momentul în care vor fi planificate.

Cerința 2: Readers-Writers
Folosind monitorul generic implementat veți rezolva problema clasică de sincronizare Readers-Writers, cu prioritate pe Writers.

Aveți o resursă partajată între N cititori si M scriitori. Resursa poate fi citită simultan de către mai mulți cititori, sau poate fi actualizată de un singur scriitor (și nici un alt scriitor nu mai poate scrie, sau alt cititor citi în timp ce acesta actualizează resursa). Orice altă combinație de cititori și scriitori nu poate opera asupra resursei simultan (combinațiile interzise sunt așadar: mai mulți cititori și cel puțin un scriitor, cel puțin 2 scriitori, un scriitor și cel puțin un cititor, și orice altă combinație care include una dintre acestea).

Rezolvarea se va prezenta sub forma unei a doua biblioteci dinamice, care oferă funcțiile:
 * Monitor* CreateRWMonitor;</tt> - Crează monitorul folosit pentru sincronizare Readers-Writers
 * <tt>int GetNrConds;</tt> - returnează numărul de variabile de condiție ale monitorului creat cu <tt>CreateRWMonitor</tt>
 * <tt>void StartCit(Monitor* m);</tt> - apelată de către un cititor atunci când acesta vrea să înceapă să citească
 * <tt>void StopCit(Monitor *m);</tt> - apelată de către un cititor pentru a marca faptul că a terminat de citit
 * <tt>void StartScrit(Monitor* m);</tt> - apelată de către un scriitor atunci când acesta vrea să înceapă să scrie
 * <tt>void StopScrit(Monitor *m);</tt> - apelată de către un scriitor pentru a marca faptul că a terminat de scris

Explicităm în continuare regulile de execuție:
 * dacă un scriitor încearcă să scrie resursa în timp ce alt scriitor o scria deja, așteaptă până când acesta din urmă termină de scris
 * dacă un scriitor găsește cititori care deja citesc resursa, așteaptă până toți aceștia termină de citit; orice cititor care va sosi după nu se va alătura celorlalți care deja citesc, ci îi va acorda prioritate scriitorului, care așteaptă
 * dacă un cititor găsește un scriitor care deja scria resursa, sau descoperă că un scriitor așteaptă să scrie resursa, nu începe citirea și așteaptă, acordând astfel prioritate scriitorilor.

Documentație
Pentru documentare, aveți la dispoziție următoarele două documente:
 * [[media:buhr95monitor.pdf|Buhr, Fortier: Monitor classification]]
 * [[media:paper5.pdf|Hoare,Monitors: An Operating System Structuring Concept]]

Primul dintre ele este foarte important. Atenție, operațiile pe care le implementați (<tt>Enter, Leave, Wait, Signal</tt>), trebuie să respecte specificațiile formale, precondițiile și postcondițiile specificate prin axiomele prezentate în primul document, pentru fiecare politică în parte. Axiomele respective vor face parte din baremul de corectare. Politica SIGNAL_AND_WAIT la care se face referire în enunțul temei se referă la politica SIGNAL AND URGENT WAIT în terminologie Buhr &amp; Fortier.

Deoarece specificațiile formale prezentate în documentație prin axiome pot fi dificil de ințeles, aveți mai jos descrierea în limbaj natural a specificațiilor monitorului. Această descriere conține și specificațiile pentru operația <tt>Broadcast</tt>, care lipsește din documentație. Cele două politici de funcționare sunt descrise la paginile 13 și 14 din documentație sub forma 4.2.2 PB și 4.2.4 PNB.

Descriere detaliată a funcționării unui monitor

 * <tt>Enter(m)</tt>
 * Dacă monitorul nu este liber:
 * Thread-ul este pus în coada Entry
 * Va fi planificat la un moment ulterior de către un alt thread, care executa "cod monitor"
 * Funcția va bloca până la planificare
 * Dacă monitorul este liber, thread-ul se va auto-planifica, continuându-și execuția și devenind "owner". Owner-ul monitorului este acel thread care execută cod "non-monitor". Owner-ul, dacă există, este unic.


 * <tt>Leave(m)</tt>
 * Thread-ul planifică pe altcineva
 * Thread-ul părăsește monitorul


 * <tt>Wait(m,c)</tt>
 * Thread-ul planifică pe altcineva
 * Thread-ul așteaptă după variabila de condiție <tt>c</tt>
 * În momentul în care este semnalizat, el va fi trecut în coada Waiting (de către altcineva)
 * La un moment ulterior, va fi planificat pentru execuție
 * Funcția va bloca până la planificare


 * <tt>Signal(m,c)</tt>
 * Se alege un thread care așteaptă după variabila de condiție <tt>c</tt> (daca există unul) și se mută în coada Waiting
 * Dacă politica este <tt>SIGNAL_AND_WAIT</tt>:
 * Thread-ul curent este pus în coada Signaller
 * Va fi planificat spre execuție din nou la un moment ulterior. Se observă că cel puțin coada Signaller este nevidă (conține cel puțin firul curent, care a apelat funcția <tt>Signal</tt>)
 * Funcția va bloca până la planificare
 * Dacă politica este <tt>SIGNAL_AND_CONTINUE</tt>:
 * Thread-ul curent iși continuă execuția, fără a elibera monitorul

Se observă că singura diferență dintre <tt>Broadcast</tt> și <tt>Signal</tt> este numărul de thread-uri mutate în coada Waiting.
 * <tt>Broadcast(m,c)</tt>
 * Toate thread-urile care așteptau după variabila de condiție <tt>c</tt> sunt mutate în coada Waiting
 * Dacă politica este <tt>SIGNAL_AND_WAIT</tt>:
 * Thread-ul curent este pus în coada Signaller
 * Va fi planificat spre execuție din nou la un moment ulterior. Se observă ca cel puțin coada Signaller este nevidă (conține cel puțin firul curent, care a apelat functia <tt>Broadcast</tt>)
 * Funcția va bloca până la planificare
 * Dacă politica este <tt>SIGNAL_AND_CONTINUE</tt>:
 * Thread-ul curent își continuă execuția, fără a elibera monitorul

Precizări Windows
Tema de Windows trebuie să implementeze cele două biblioteci partajate ca DLL-uri, sub numele de <tt>LibMonitor.DLL</tt> și <tt>LibRW.DLL</tt>. Pentru instrucțiuni de construire a unui DLL, consultați Platform SDK.

Datorită faptului că în Windows DLL-urile nu pot conține simboluri nedefinite, în momentul în care construiți DLL-ul va trebuie să-l link-ați cu <tt>ControlMonitor.obj</tt> și <tt>ControlRW.obj</tt>, fișiere obținute din pasul 1 de compilare al testelor. Exemplu: build: LibMonitor.obj LibRW.obj link /release /dll /out:LibMonitor.dll LibMonitor.obj  ControlMonitor.obj link /release /dll /out:LibRW.dll LibRW.obj ControlRW.obj LibMonitor.lib

Pentru a testa utilizarea corectă a monitorului, mentineți un index în Thread Local Storage care indică pentru fiecare thread dacă se află sau nu în interiorul monitorului. Nu e necesar să alocați memorie, puteți folosi doar valoarea pointerului din indexul TLS. Pentru indicații privind Thread local Storage pe Windows consultați următoarele link-uri din Platform SDK:


 * Thread Local Storage
 * Using Thread Local Storage

Nu este permisă folosirea funcției PulseEvent în implementarea temei.

Tema se va rezolva folosind doar funcții Win32. Se pot folosi de asemenea și funcțiile de formatare <tt>printf, scanf</tt>, funcțiile de alocare de memorie <tt> malloc, free</tt> și funcțiile de manipulare a șirurilor de caractere (<tt>strcat</tt>, <tt>strdup</tt>, etc.)

Pentru partea de I/O și procese se vor folosi doar funcții Win32. De exemplu, funcțiile <tt>open</tt>, <tt>read</tt>, <tt>write</tt>, <tt>close</tt> nu trebuie folosite, în locul acestor trebuind să folosiți <tt>CreateFile</tt>, <tt>ReadFile</tt>, <tt>WriteFile</tt>, <tt>CloseHandle</tt>.

Precizari Linux
Tema de Linux trebuie să compileze cele două biblioteci partajate folosind numele <tt>LibMonitor.so</tt> și <tt>LibRW.so</tt>. O bibliotecă partajată se crează folosind opțiunea <tt> -fPIC</tt> la compilare pentru generarea de cod independent de pozitie și optiunea <tt> -shared</tt> la linkare. Pentru infomații mai detaliate despre biblioteci, consultați Program Library HOWTO.

Pentru a testa utilizarea corectă a monitorului, mențineți un index în Thread Specific Data care indică pentru fiecare fir de execuție dacă se află sau nu în interiorul monitorului. Nu e necesar să alocați memorie, puteți folosi doar valoarea pointerului din indexul TSD.

Pentru cazurile în care vă confruntați cu deadlock-uri, puteți folosi <tt>gdb</tt> pentru debugging-ul programului în timp ce acesta rulează:
 * <tt>gdb ./myprogram pid</tt> - după ce, în prealabil, ați obținut pid-ul procesului (<tt>ps ax | grep myprogram</tt>)
 * <tt>info threads</tt> - informații despre thread-urile procesului
 * <tt>thread N</tt> - comutare la thread-ul <tt>N</tt>
 * <tt>bt</tt> - backtrace, informații despre stiva de apeluri

Tema se va rezolva folosind doar funcții POSIX. Se pot folosi de asemenea și funcțiile de formatare <tt>printf, scanf</tt>, funcțiile de alocare de memorie <tt> malloc, free</tt>, și funcțiile de manipulare a șirurilor de caractere (<tt>strcat</tt>, <tt>strdup</tt>, etc.)

Tema se va rezolva folosind fire de execuție POSIX și exclusiv mecanisme de sincronizare a firelor de execuție POSIX.

Pentru partea de I/O și procese se vor folosi doar funcții POSIX. De exemplu, funcțiile <tt>fopen</tt>, <tt>fread</tt>, <tt>fwrite</tt>, <tt>fclose</tt> nu trebuie folosite, în locul acestora trebuind să folosiți <tt>open</tt>, <tt>read</tt>, <tt>write</tt>, <tt>close</tt>.

Testare
Programul de test (linux, windows) folosește funcțiile oferite de cele două biblioteci pentru a implementa 3 teste:


 * TestMonitor verifică funcționarea corectă a monitorului
 * TestRW verifică funcționarea corectă a funcțiilor scriitorilor și cititorilor
 * TestStres verifică funcționarea monitorului / scriitorilor și cititorilor în condiții de încărcare puternică

Pentru testarea celor două biblioteci va trebui să apelați niște funcții care vor verifica corectitudinea stării monitorului și a resursei care este citită și scrisă. Voi trebuie să decideți, în funcție de implementarea voastră, unde anume să inserați apelurile lor în cod. Iată lista funcțiilor și semnificația lor:


 * Functii pentru testarea monitorului generic:
 * <tt>void IncEnter;</tt> - un thread a fost pus în coada Entry Queue
 * <tt>void DecEnter;</tt> - un thread a fost scos din Entry Queue
 * <tt>void IncSignal;</tt> - un thread a fost pus în coada Signaller Queue
 * <tt>void DecSignal;</tt> - un thread a fost scos din Signaller Queue
 * <tt>void IncWait;</tt> - un thread a fost pus în coada Waiting Queue
 * <tt>void DecWait;</tt> - un thread a fost scos din coada Waiting Queue
 * <tt>void IncCond(int nrCond);</tt> - a fost pus un thread în coada de așteptare a variabilei de condiție nrCond
 * <tt>void DecCond(int nrCond);</tt> - a fost scos un thread din coada de așteptare a variabilei de condiție nrCond
 * Funcții pentru testarea Readers-Writers:
 * <tt>void AnnounceStartCit;</tt> - când un cititor începe efectiv să citească (deci NU când intenționează să citească, ci ulterior, când începe efectiv să citească)
 * <tt>void AnnounceStopCit;</tt> - când un cititor se oprește efectiv din citit
 * <tt>void AnnounceStartScrit;</tt> - când un scriitor începe efectiv să scrie
 * <tt>void AnnounceStopScrit;</tt> - când un scriitor se oprește efectiv din scris

Antelele acestor funcții se găsesc în [[media:CallbackMonitor.h|CallbackMonitor.h]], respectiv [[Media:CallbackRW.h|CallbackRW.h]]. În [[Media:Constante.h|Constante.h]] găsiți constantele pentru politicile de funcționare ale monitorului. Pentru a rula tema, executați următoarea secvență de comenzi, după ce ați dezarhivat testele în directorul cu sursele temei:

Testele pe Windows au extensia <tt>.cpp</tt> si se recomanda ca sursele voastre sa urmeze aceeasi conventie, pentru a evita problemele la link-are.

Notare
Nota maximă este de 100p.

Testele care validează rezolvarea voastră sunt formate din:
 * 6 teste "Monitor", fiecare a 6.5 puncte
 * 5 teste "RW", fiecare a 6.5 puncte
 * 3 teste "Stress", fiecare a 9.5 puncte

Un test este considerat trecut numai dacă toți pașii din care este format se termină cu succes.

Depunctări suplimentare:


 * -0.6 erori de sincronizare (deadlock, race condition, etc.)
 * -0.4 neverificarea condițiilor de eroare sau/și neeliberarea de resurse
 * -0.1 pentru Makefile incorect
 * -0.2 pentru README necorespunzator
 * -0.2 pentru surse prost comentate
 * -0.1 diverse alte probleme constatate in implementare

Întrebări
Pentru lămuriri suplimentare asupra temei consultați arhivele listei de discuții sau dați un [mailto:so@cursuri.cs.pub.ro mail] (trebuie să fiți înregistrați ).