This is an old revision of the document!
Pentru parcurgerea demo-urilor, folosim arhiva aferentă. Demo-urile rulează pe Linux. Descărcăm arhiva folosind comanda
wget http://elf.cs.pub.ro/so/res/cursuri/curs-09-demo.zip
și apoi decomprimăm arhiva
unzip curs-09-demo.zip
și accesăm directorul rezultat în urma decomprimării
cd curs-09-demo/
Acum putem parcurge secțiunile cu demo-uri de mai jos.
Dorim să urmărim ce se întâmplă în cazul în care avem date partajate într-un mediu multithread. Pentru aceasta accesăm subdirectorul list-excl/; urmărim conținutul fișierelor thread-list-app.c și list.c. Este vorba de o aplicație care lucrează cu liste înlănțuite într-un mediu multithreaded. Vom observa că există riscul ca datele să fie corupte, fiind necesară sincronizare.
Compilăm fișierele folosind make. Rezultă două fișiere în format executabil: thread-list-app și thread-list-app-mutex.
Programul thread-list-app-mutex folosește intern un mutex pentru sincronizare. Pentru aceasta folosim macro-ul USE_MUTEX pe care îl definim în fișierul Makefile.
Ca să urmărim ce se întâmplă cu o aplicație multithreaded cu date partajate, rulăm de mai multe ori programul thread-list-app, până la obținerea unei erori:
./thread-list-app
thread-list-app.c macro-urile NUM_THREADS și NUM_ROUNDS la alte valori (probabil mai mari). Apoi recompilăm folosind make și rulăm din nou programul thread-list-app.
Eroare este cauzată de coruperea pointerilor din lista înlănțuită a programului. Datele sunt accesate concurent, iar în absența sincronizării, vor fi corupte.
Soluția este să folosim un mutex pentru protejarea accesului la listă, lucru realizat în programul thread-list-app-mutex. Dacă vom rula de mai multe ori programul thread-list-app-mutex, nu vom obține niciodată eroare:
./thread-list-app-mutex
Avem acces exclusiv la datele partajate deci am rezolvat problema coruperii datelor din cauza accesului concurent neprotejat.
Dorim să investigăm overheadul produs când folosim spinlock-uri și mutex-uri pentru acces exclusiv. Pentru aceasta accesăm subdirectorul spinlock-mutex/; urmărim conținutul fișierului spinlock-mutex.c. Acest fișiere folosește spinlock-uri sau mutex-uri pentru asigurarea accesului exclusiv.
Compilăm cele două programe folosind make. Rezultă două fișiere în format executabil: spinlock și mutex.
spinlock-mutex/.spin.c.USE_SPINLOCK.Makefile.USE_SPINLOCK.make pentru a obține două executabile: spin și mutex.time pentru a contabiliza timpul de rulare:time ./spin time ./mutex
./spin nu petrece foarte mult în kernel space (system time – sys în output-ul time)?
Dorim să urmărim cum se manifestă o condiție de cursă de tipul TOCTTOU (time of check to time of use). Pentru aceasta accesăm subdirectorul tocttou/; urmărim conținutul fișierului tocttou.c. Fișierul simulează o problemă producător consumator în care producătorul produce câte NUM_ITEMS_PRODUCER elemente, iar consumatorul consumă câte NUM_ITEMS_CONSUMER elemente; dacă un consumator nu poate consuma elemente, își încheie execuția.
Compilăm programul folosind make. Rezultă fișierul tocttou în format executabil.
Dorim să urmărim cum se manifestă un deadlock. Pentru aceasta accesăm subdirectorul deadlock/; urmărim conținutul fișierului deadlock.c. Fișierul are o implementare didactică pentru două tipuri de thread-uri care obțin în ordine diferită două mutex-uri.
Compilăm programul folosind make. Rezultă fișierul deadlock în format executabil.
Dorim să urmărim cum se manifestă o problemă de așteptare nedefinită (indefinite wait). Pentru aceasta accesăm subdirectorul indefinite-wait/; urmărim conținutul fișierului indefinite-wait.c. Fișierul are o implementare nefucțională pentru problema producători-consumatori, lucru care conduce la așteptare nedefinită.
Compilăm programul folosind make. Rezultă fișierul indefinite-wait în format executabil.
Dorim să urmărim cum se comportă un program atunci când diferă dimensiunea regiunii critice folosită pentru acces exclusiv. Pentru aceasta accesăm subdirectorul granularity/; urmărim conținutul fișierului granularity.c. Fișierul are o implementare didactică în care mai multe thread-uri accesează o regiune critică.
Compilăm programul folosind make. Rezultă două fișiere în format executabil: granuarity-fine și granularity-coarse. Fișierului executabil granularity-fine îi corespunde o regiune critică de mică dimensiune, dar accesată des; fișierului executabil granularity-coarse îi corespunde o regiune critică de dimensiune mai mare, dar accesată rar.
În cadrul codului, folosirea unei granularități fine sau nu este indicată de macro-ul GRANULARITY_TYPE inițializat în Makefile.
Pentru a vedea impactul tipului de granularitate, rulăm cele două fișiere în format executabil și măsurăm timpul de rulare:
/usr/bin/time ./granularity-fine /usr/bin/time ./granularity-coarse
Observăm că dureaza semnificativ mai mult rularea executabilului cu granularitate fină. Acest lucru se întâmplă pentru că regiunea critică pe care acesta o protejează este mic, iar cea mai mare parte din timp o va consuma în operațiile cu mutex-ul (lock contention). Observăm acest lucru și din numărul mare de schimbări de context (voluntare sau nevoluntare).