Sari la conținutul principal

Laboratorul 2 - Elemente de sincronizare în Pthreads

Introducere

Când vorbim de programarea paralelă, putem avea situații în care mai multe fire de execuție care rulează în paralel vor să acceseze simultan aceleași resurse. De exemplu, putem avea situația următoare. Avem două thread-uri (T0 și T1) care au acces partajat la o variabilă întreagă a inițializată cu 0. În funcția de thread, atât T0 cât și T1 incrementează a cu 2. În mod normal, ne-am aștepta ca valoarea variabilei a după execuția programului nostru să fie 4, pentru că avem două incrementări ale variabilei în cele două thread-uri.

În realitate, situația nu este întotdeauna așa. Dacă am traduce incrementarea unei variabile întregi în cod de asamblare, această operație ar putea arăta în felul următor (în exemplul de mai jos, eax0 reprezintă registrul eax al thread-ului T0, iar eax1 desemnează registrul eax al thread-ului T1):

T0T1
load(a, eax0)load(a, eax1)
eax0 = eax0 + 2eax1 = eax1 + 2
write(a, eax0)write(a, eax1)

Putem avea următorul scenariu:

  • T0 citește valoarea lui a (0) în propriul registru eax0 în același timp, T1 citește valoarea lui a (tot 0) în propriul registru eax1
  • T0 incrementează valoarea lui eax0, care devine 2
  • T1 face același lucru, iar eax1 devine tot 2
  • T0 scrie valoarea din eax0 în a, care devine 2
  • T1 scrie valoarea din eax1 în a, care rămâne tot 2.

Se poate deci observa că, în funcție de modul în care thread-urile T0 și T1 sunt planificate, este posibil ca rezultatul secvenței de mai sus să fie 2 sau 4. Acest lucru se numește race condition și este cauzat de faptul că rezultatul calculului este condiționat de modul de planificare a unor evenimente necontrolabile. Operația de incrementare a lui a cu 2 nu este atomică, fiind compusă din mai multe operații care se pot intercala atunci când rulăm pe mai multe fire de execuție.

📄️ Barieră

O altă primitivă de sincronizare folosită în calculul paralel este bariera. Ea are rolul de a se asigura că niciun thread nu poate trece mai departe de punctul în care este plasată decât atunci când toate thread-urile gestionate de barieră ajung în acel punct. Un exemplu de utilizare este atunci când împărțim un calcul pe mai multe thread-uri și vrem să nu mergem mai departe cu execuția programului decât în momentul în care fiecare thread și-a terminat propriile calcule.