Laboratorul 1 - Introducere în programarea paralelă cu Pthreads
Programarea paralelă
Până acum, programele pe care le-ați implementat au urmat modelul de calcul serial. Astfel, o problemă era împărțită într-un set de instrucțiuni care erau executate secvențial (una după alta), la un moment dat executându-se o singură instrucțiune din cadrul programului, acesta neprofitând de puterea de calcul a mai multor procesoare.
În acest semestru una dintre tematicile principale studiate este programarea paralelă, adică utilizarea simultană a mai multor resurse de calcul pentru a rezolva o problemă mai rapid.
Cum abordăm paralelizarea unei probleme?
Atunci când vrem să paralelizăm o problemă, urmăm câțiva pași:
-
Împărțim problema în componente independente
-
Împărțim fiecare componentă în instrucțiuni
-
Executăm componentele simultan
Instrucțiunile subproblemelor diferite pot fi rulate în paralel, folosind procesoare separate.
Totuși, în practică nu toate componentele sunt complet independente și pentru a putea gestiona astfel de situații corect este necesar un mecanism de coordonare și sincronizare a execuției.
Când merită să paralelizăm o problemă?
O problemă poate fi paralelizată eficient dacă:
- Poate fi împărțită logic în componente care se pot executa simultan
- Execuția paralelă scade timpul total de rulare, comparativ cu execuția serială
- Avem hardware corespunzător:
- fie o mașină de calcul cu mai multe procesoare/core-uri
- fie mai multe calculatoare conectate printr-o rețea (nu mai vorbim strict de programare paralelă, ci de programare distribuită)
Întrebări de design
Când implementăm un program paralel, trebuie să ne gândim la:
- Cum partiționăm problema?
- Cum balansăm încărcarea? (evităm situațiile în care unul muncește, iar ceilalți stau degeaba)
- Cum comunică între ele componentele care rulează în paralel și cum le sincronizăm?
- Ce dependențe de date există între componente?
- Cât de mare este efortul de a paraleliza și dacă se justifică?
De-a lungul acestui semestru vom încerca să răspundem la aceste întrebări și să exersăm prin exemple practice.
📄️ Fire de execuție
Un fir de execuție (sau thread în engleză) este definit ca un flux independent de instrucțiuni care pot fi planificate de către sistemul de operare. Din punct de vedere al unui programator, un fir de execuție poate fi descris cel mai bine ca o funcție care rulează independent de programul principal, iar un program paralel (cu mai multe fire de execuție, sau multi-threaded) poate fi privit ca toată mulțimea de astfel de funcții care pot fi planificate să ruleze simultan și/sau independent de către sistemul de operare.
📄️ Pthreads (thread-uri POSIX)
La începutul programării paralele, producătorii de hardware își implementau propriile versiuni de thread-uri, care difereau considerabil între ele, ceea ce ducea la dificultăți în dezvoltarea de aplicații multi-threaded portabile. Din acest motiv, o interfață standardizată de programare multi-thread a fost necesară, iar acest lucru a fost specificat, pentru sisteme UNIX, de către standardul IEEE POSIX 1003.1c în anul 1995. Astfel, implementările de thread-uri care aderă la acest standard sunt denumite thread-uri POSIX, sau Pthreads, iar astăzi majoritatea furnizorilor de hardware oferă și Pthreads.
📄️ Exerciții
1. Compilați și executați codul din fișierul example.c din scheletul de laborator, pe care îl găsiți pe acest repository de GitHub, pe care trebuie să îl clonați, folosind comanda `git clone`. Schimbați numărul de thread-uri și observați cum se schimbă comportamentul programului.