This shows you the differences between two versions of the page.
so:cursuri:curs-09 [2014/04/14 21:13] razvan.deaconescu [Deadlock] |
so:cursuri:curs-09 [2019/04/15 01:04] (current) razvan.deaconescu |
||
---|---|---|---|
Line 1: | Line 1: | ||
====== Curs 09 - Sincronizare ====== | ====== Curs 09 - Sincronizare ====== | ||
- | <html> | + | * [[http://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf|Curs 09 - Sincronizare (PDF)]] |
- | <iframe src="http://docs.google.com/viewer?url=http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf&embedded=true" width="600" height="480" style="border: none;"> | + | |
- | </iframe> | + | |
- | </html> | + | |
- | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf | Curs 09 - Sincronizare (PDF)]] | + | * [[https://drive.google.com/open?id=1PCJp2UD0KSuAOFJ9ZMkB_lMhR9jJ4exD7Pgm-vzSrQA|Notițe de curs]] |
* Suport curs | * Suport curs | ||
- | * Operating Systems Concepts | + | * Operating Systems Concepts Essentials |
* Capitolul 6 - Process Synchronization | * Capitolul 6 - Process Synchronization | ||
* Modern Operating Systems | * Modern Operating Systems | ||
Line 15: | Line 12: | ||
* Secțiunea 2.3 - Interprocess Communication | * Secțiunea 2.3 - Interprocess Communication | ||
* [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | * [[http://greenteapress.com/semaphores/|Allen B. Downey - The Little Book of Semaphores]] | ||
+ | * [[https://deadlockempire.github.io/|The Deadlock Empire: Slay dragons, master concurency!]] | ||
+ | * [[http://elf.cs.pub.ro/so/res/cursuri/SO_Curs-09.pdf|Curs 09 anterior]] | ||
+ | |||
+ | |||
+ | <html> | ||
+ | <center> | ||
+ | <iframe src="https://docs.google.com/viewer?url=https://elf.cs.pub.ro/so/res/cursuri/SO%20-%20Curs%2009%20-%20Sincronizare.pdf&embedded=true" width="600" height="480" style="border: none;"> | ||
+ | </iframe> | ||
+ | </center> | ||
+ | </html> | ||
===== Demo-uri ===== | ===== Demo-uri ===== | ||
Line 44: | Line 51: | ||
</note> | </note> | ||
- | 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. | + | Eroarea 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:<code bash> | 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:<code bash> | ||
Line 58: | Line 65: | ||
Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | Compilăm cele două programe folosind ''make''. Rezultă două fișiere în format executabil: ''spinlock'' și ''mutex''. | ||
- | Cele două fișiere au fost generat din același cod sursă (''spinlock-mutex.c''), după cum a fost definit sau nu macro-ul ''USE_SPINLOCK''. Macro-ul îl definim în fișierul ''Makefile''. | + | Cele două fișiere au fost generate din același cod sursă (''spinlock-mutex.c''), după cum a fost definit sau nu macro-ul ''USE_SPINLOCK''. Macro-ul îl definim în fișierul ''Makefile''. |
Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''time'':<code bash> | Pentru a contabiliza timpul de rulare rulăm cele două executabile prin comanda ''time'':<code bash> | ||
Line 92: | Line 99: | ||
adică o condiție de cursă de tipul TOCTTOU, în care se poate "infiltra" alt thread. Practic este posibil ca două thread-uri să scadă valoarea variabilei ''num_items'' deși condiția ar fi trebuit să se întâmple doar pentru unul. Dacă avem o valoare inițială ''NUM_ITEMS_CONSUMER + 2'', atunci există riscul ca mai multe thread-uri să vadă îndeplinită condiția și apoi toate să scadă valoarea variabilei ''num_items'' rezultând într-o valoare negativă. | adică o condiție de cursă de tipul TOCTTOU, în care se poate "infiltra" alt thread. Practic este posibil ca două thread-uri să scadă valoarea variabilei ''num_items'' deși condiția ar fi trebuit să se întâmple doar pentru unul. Dacă avem o valoare inițială ''NUM_ITEMS_CONSUMER + 2'', atunci există riscul ca mai multe thread-uri să vadă îndeplinită condiția și apoi toate să scadă valoarea variabilei ''num_items'' rezultând într-o valoare negativă. | ||
- | O astfel de situația poate duce la un comportament nedeterminst al programului, la coruperea datelor, chiar la încheierea cu eroare a programului. Mai mult o astfel de situație poate fi exploatată de un atacator. | + | O astfel de situație poate duce la un comportament nedeterminst al programului, la coruperea datelor, chiar la încheierea cu eroare a programului. Mai mult o astfel de situație poate fi exploatată de un atacator. |
Pentru a preveni apariția condițiilor de cursă, trebuie implementată corespunzător sincronizarea accesului. Din păcate, condițiile de cursă pot apărea foarte greu (adică programul să fie greșit dar să meargă **aproape** tot timpul); acest lucru face nedeterminist comportamentul programului și dificilă investigarea problemei. | Pentru a preveni apariția condițiilor de cursă, trebuie implementată corespunzător sincronizarea accesului. Din păcate, condițiile de cursă pot apărea foarte greu (adică programul să fie greșit dar să meargă **aproape** tot timpul); acest lucru face nedeterminist comportamentul programului și dificilă investigarea problemei. | ||
Line 108: | Line 115: | ||
Deadlock-ul are loc pentru că cele două mutex-uri folosite în program (''xmutex'' și ''ymutex'') nu sunt achiziționate în aceeași ordine. Unele thread-uri (cele care execută funcția ''xfirst'') achiziționează mutex-urile în ordinea ''xmutex'', ''ymutex''; celelalte thread-uri (cele care execută funcția ''yfirst'') achiziționează mutex-urile în ordinea ''ymutex'', ''xmutex''. În acest fel, la un moment dat un thread va achiziționa mutex-ul ''xfirst'' și imediat după un altul va achiziționa ''ymutex''. În continuare ambele thread-uri vor aștepta după un mutex deținut de alt thread. | Deadlock-ul are loc pentru că cele două mutex-uri folosite în program (''xmutex'' și ''ymutex'') nu sunt achiziționate în aceeași ordine. Unele thread-uri (cele care execută funcția ''xfirst'') achiziționează mutex-urile în ordinea ''xmutex'', ''ymutex''; celelalte thread-uri (cele care execută funcția ''yfirst'') achiziționează mutex-urile în ordinea ''ymutex'', ''xmutex''. În acest fel, la un moment dat un thread va achiziționa mutex-ul ''xfirst'' și imediat după un altul va achiziționa ''ymutex''. În continuare ambele thread-uri vor aștepta după un mutex deținut de alt thread. | ||
- | <note tip> | + | <note important> |
Pentru a evita apariția deadlock-urilor în momentul în care folosim lock-uri/mutex-uri, o condiție importantă este ca lock-urile să fie achiziționate în aceeași ordine. | Pentru a evita apariția deadlock-urilor în momentul în care folosim lock-uri/mutex-uri, o condiție importantă este ca lock-urile să fie achiziționate în aceeași ordine. | ||
</note> | </note> | ||
Line 116: | Line 123: | ||
Compilăm programul folosind ''make''. Rezultă fișierul ''indefinite-wait'' în format executabil. | Compilăm programul folosind ''make''. Rezultă fișierul ''indefinite-wait'' în format executabil. | ||
+ | |||
+ | Pentru a observa comportamentul programului îl rulăm:<code bash> | ||
+ | ./indefinite-wait | ||
+ | </code> | ||
+ | Observăm că programul se blochează, deci fie avem deadlock fie unele thread-uri așteaptă fără a fi trezite/notificate. Având un singur mutex folosit corezpunzător, problema nu poate fi deadlock deci este vorba de o așteptare nedefinită. | ||
+ | |||
+ | La o investigație atentă observăm că atât thread-urile producător cât și cele consumator vor aștepta la variabilele condiție aferente (''buffer_empty_cond'' și ''buffer_full_cond'') fără a fi trezite. Nu există apeluri ale funcției [[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_cond_signal.html|pthread_cond_signal]] necesare pentru trezirea thread-urilor. | ||
+ | |||
+ | Rezolvăm această problemă prin adăugarea apelurilor necesare în cadrul funcțiilor ''produce'' și ''consume'' rezultând forma actualizată:<code c> | ||
+ | static void *produce(void *arg) | ||
+ | { | ||
+ | size_t idx = (size_t) arg; | ||
+ | |||
+ | pthread_mutex_lock(&pc_mutex); | ||
+ | if (pc_buffer.size >= MAX_ITEMS) | ||
+ | pthread_cond_wait(&buffer_full_cond, &pc_mutex); | ||
+ | pc_buffer.storage[pc_buffer.size] = ITEM; | ||
+ | pc_buffer.size++; | ||
+ | pthread_cond_signal(&buffer_empty_cond); | ||
+ | printf("Producer %zu created item.\n", idx); | ||
+ | pthread_mutex_unlock(&pc_mutex); | ||
+ | |||
+ | return NULL; | ||
+ | } | ||
+ | |||
+ | static void *consume(void *arg) | ||
+ | { | ||
+ | size_t idx = (size_t) arg; | ||
+ | |||
+ | pthread_mutex_lock(&pc_mutex); | ||
+ | if (pc_buffer.size == 0) | ||
+ | pthread_cond_wait(&buffer_empty_cond, &pc_mutex); | ||
+ | pc_buffer.storage[pc_buffer.size] = NO_ITEM; | ||
+ | pc_buffer.size--; | ||
+ | pthread_cond_signal(&buffer_full_cond); | ||
+ | printf("Consumer %zu removed item.\n", idx); | ||
+ | pthread_mutex_unlock(&pc_mutex); | ||
+ | |||
+ | return NULL; | ||
+ | } | ||
+ | </code> | ||
+ | În cele de mai sus, înainte de operația de unlock pe mutex am apelat funcția [[http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_cond_signal.html|pthread_cond_signal]]. Cu ajutorul acestei funcții, thread-ul producător ce tocmai a produs un element trezește un thread consumator care acum are ce consuma; în mod similar thread-ul consumator care a consumat un element trezește un thread producător care acum are loc unde să producă un element. | ||
+ | |||
+ | După actualizarea programului, recompilăm folosind ''make'' și apoi îl rulăm. Acum programul va funcționa corespunzător fără problema așteptării nedefinite. | ||
==== Granularitate regiune critică ==== | ==== Granularitate regiune critică ==== | ||
Line 129: | Line 180: | ||
</code> | </code> | ||
- | 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). | + | 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). |
<note> | <note> | ||
În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | În general preferăm granularitate fină pentru regiunile critice (adică regiuni critice de mică dimensiune); dar granularitatea fină poate înseamna operații foarte dese pe mutex (//lock contention//). De aceea este recomandat ca folosirea granularității fine să fie echilibrată de un număr redus de thread-uri care să dorească să acceseze la un moment dat regiunea critică, pentru a genera cât mai puțină încărcare (//contention//) pe mutex. | ||
</note> | </note> | ||
- |