Differences

This shows you the differences between two versions of the page.

Link to this comparison view

app:laboratoare:04 [2022/10/23 17:46]
florin.mihalache
app:laboratoare:04 [2024/10/29 02:44] (current)
alexandru.bala [Variabile condiție]
Line 1: Line 1:
-====== Laboratorul 4 - MPI ====== +====== Laboratorul 4 - PThreads ​======
-===== Despre MPI ===== +
-MPI (Message Passing Interface) reprezintă un standard pentru comunicarea prin mesaje în cadrul programării distribuite,​ elaborat de MPI Forum, și are la bază modelul proceselor comunicante prin mesaje.+
  
-Un proces ​reprezintă ​un program aflat în execuție și se poate defini ca unitate de bază care poate executa una sau mai multe sarcini în cadrul unui sistem de operare. Spre deosebire de thread-uriun proces are propriul său spațiu de adrese (propria zonă de memorie) și acesta poate avea, în cadrul său, mai multe thread-uri în execuție, care partajează resursele procesului.+===== Despre pthreads ===== 
 +pthreads ​reprezintă o bibliotecă din C/C++nativă Linuxprin care se pot implementa programe multithreaded.
  
-===== Compilare ​și rulare ===== +Spre deosebire de OpenMP, pthreads este low-level ​și oferă o mai mare flexibilitate în ceea ce privește sincronizarea thread-urilor și distribuirea task-urilor către thread-uri.
-În cadrul lucrului în C/C++, MPI reprezintă o bibliotecă,​ care are funcționalitățile implementate într-un header numit **mpi.h**. Pentru compilare, la MPI există un compilator specific: +
-  * ''​mpicc'',​ pentru lucrul în C +
-  * ''​mpic++'',​ pentru lucrul în C++ +
-În ambele limbaje, pentru rularea unui program MPI folosim comanda ''​mpirun'',​ împreună cu parametrul ''​-np'',​ unde precizăm numărul de procese care rulează în cadrul programului distribuit.+
  
-Exemplu: +===== Implementarea unui program paralel în pthreads ===== 
-  ​* ​compilare: +==== Includere și compilare ​==== 
-    * C: ''​mpicc hello.c -o hello''​ +Pentru a putea folosi pthreads, este necesar să includem în program biblioteca ​''​pthread.h''​. ​De asemenea la compilare este necesar să includem flag-ul ''​-lpthread''​
-    * C++: ''​mpic++ hello.cpp -o hello''​ +<code bash> 
-  * rulare: ​''​mpirun ​-np 4 hello'' ​- rulare cu 4 procese +gcc -o program program.-lpthread 
-Dacă încercați să rulați comanda mpirun cu un număr de procese mai mare decât numărul de core-uri fizice disponibile pe procesorul vostru, este posibil să primiți ​eroare cum ca nu aveți destule sloturi liberePuteți elimina acea eroare adăugând parametrul ''​--oversubscribe''​ atunci când rulați ''​mpirun''​.+./program 
 +</​code>​
  
-===== Instalare MPI ===== +==== Crearea și terminarea thread-urilor ​==== 
-Pentru a lucra cu MPItrebuie să instalați biblioteca pentru MPI pe Linux, folosind următoarea comandă: ''​sudo apt install openmpi-bin openmpi-common openmpi-doc libopenmpi-dev''​+În pthreadsavem un thread principal, pe care rulează funcția main. Din thread-ul principal se pot crea thread-uri noi, care vor executa task-uri în paralel.
  
-===== Implementarea unui program distribuit în MPI ===== +Pentru a crea thread-uri în pthreads, folosim funcția ''​pthread_create''​:
-Exemplu de program MPI Hello World:+
 <code c> <code c>
-#​include ​"mpi.h"+int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*thread_function) (void *), void *arg); 
 +</​code>​ 
 +unde: 
 +  * ''​thread''​ - thread-ul pe care vrem să-l pornim 
 +  * ''​attr''​ - atributele unui thread (''​NULL''​ - atribute default) 
 +  * ''​thread_function''​ - funcția pe care să o execute thread-ul 
 +  * ''​arg''​ - parametrul trimis la funcția executată de thread (dacă vrem să trimitem mai mulți parametri, îi împachetăm într-un struct) 
 + 
 +Exemplu de funcție pe care o execută un thread: 
 +<code c> 
 +void *f(void *arg) { 
 +    // do stuff 
 +    // aici putem să întoarcem un rezultat, dacă este cazul 
 +    pthread_exit(NULL);​ // termină un thread - mereu apelat la finalul unei funcții executate de thread, dacă nu întoarcem un rezultat în funcție 
 +
 +</​code>​ 
 +Pentru terminarea thread-urilor,​ care vor fi "​lipite înapoi"​ în thread-ul principal, folosim funcția ''​pthread_join'',​ care așteaptă terminarea thread-urilor:​ 
 +<code c> 
 +int pthread_join(pthread_t thread, void **retval);​ 
 +</​code>​ 
 +unde: 
 +  * ''​thread''​ - thread-ul pe care îl așteptăm să termine 
 +  * ''​retval''​ - valoarea de retur a funcției executate de thread (poate fi ''​NULL''​) 
 + 
 +Exemplu de program scris folosind pthreads: 
 +<code c> 
 +#​include ​<pthread.h>
 #include <​stdio.h>​ #include <​stdio.h>​
 #include <​stdlib.h>​ #include <​stdlib.h>​
    
-#​define ​ ​MASTER 0+#​define ​NUM_THREADS 2
    
-int main (int argc, char *argv[]) { +void *f(void *arg) 
-    ​int numtasks, ranklen+
-    ​char hostname[MPI_MAX_PROCESSOR_NAME];+    ​long id = *(long*) arg; 
 +    printf("​Hello World din thread-ul %ld!\n"​id)
 +    ​return NULL; 
 +}
    
-    MPI_Init(&argc, &argv); +int main(int argc, char *argv[]) 
-    ​MPI_Comm_size(MPI_COMM_WORLD,​ &​numtasks)+{ 
-    ​MPI_Comm_rank(MPI_COMM_WORLD,&​rank)+    ​pthread_t threads[NUM_THREADS]
-    ​MPI_Get_processor_name(hostname,​ &len)+    ​int r
-    ​if (rank == MASTER) +    ​long id
-        printf("​MASTER:​ Number of MPI tasks is: %d\n",​numtasks)+    ​void *status
-    ​else +    ​long arguments[NUM_THREADS];
-        printf("​WORKER:​ Rank: %d\n",​rank);+
    
-    ​MPI_Finalize();+    ​for (id = 0; id < NUM_THREADS;​ id++
 +        arguments[id] = id; 
 +        r = pthread_create(&​threads[id],​ NULL, f, (void *) &​arguments[id]);​ 
 +  
 +        if (r) { 
 +            printf("​Eroare la crearea thread-ului %ld\n",​ id); 
 +            exit(-1); 
 +        } 
 +    } 
 +  
 +    for (id = 0; id < NUM_THREADS;​ id++) { 
 +        r = pthread_join(threads[id],​ &​status);​ 
 +  
 +        if (r) { 
 +            printf("​Eroare la asteptarea thread-ului %ld\n",​ id); 
 +            exit(-1); 
 +        } 
 +    } 
 +  
 +    return 0;
 } }
 </​code>​ </​code>​
  
-Un comunicator (''​MPI_Comm''​) reprezintă un grup de procese care comunică între ele. ''​MPI_COMM_WORLD'' ​reprezintă comunicatorul defaultdin care fac parte toate procesele.+În caz că dorim să trimitem mai mulți parametri funcției executate de threads, folosim ​un ''​struct''​, în care incapsulăm dateleși îl trimitem ca parametru al funcției executate de threads.
  
-Funcții+Exemplu
-  * ''​MPI_Init''​ - se inițializează programul MPI, mai precis se creează contextul în cadrul ​căruia rulează procesele. Argumentele din linie de comandă sunt pasate către contextul de rulare a proceselor+<spoiler Click pentru exemplu>​ 
-  * ''​MPI_Comm_size''​ - funcție care determină numărul de procese (numtasks) care rulează în cadrul comunicatorului (de regulă MPI_COMM_WORLD) +<​code ​c
-  * ''​MPI_Comm_rank''​ - funcție care determină identificatorul (rangul) procesului curent în cadrul comunicatorului+#include <pthread.h> 
-  * ''​MPI_Get_processor_name''​ - determină numele procesorului +#include <​stdio.h>​ 
-  * ''​MPI_Finalize''​ - declanșează terminarea programului MPI+#include <stdlib.h> 
 +  
 +#define NUM_THREADS 8
  
-În cadrul schimbului de date între proceseeste necesar mereu să precizăm tipul acestoraÎn MPIse folosește enum-ul ''​MPI_Datatype''​care se mapează cu tipurile de date din C/C++, după cum puteți vedea în tabelul de mai jos:+struct pair { 
 +    int firstsecond; 
 +}; 
 +  
 +void *f(void *arg) 
 +
 +    struct pair info = *(struct pair*) arg; 
 +    printf("​First = %d; second = %d\n", info.firstinfo.second);​ 
 +    pthread_exit(NULL);​ 
 +
 +  
 +int main(int argc, char *argv[]) 
 +
 +    pthread_t threads[NUM_THREADS];​ 
 +    int r; 
 +    long id; 
 +    void *status; 
 +    struct pair arguments[NUM_THREADS];​ 
 +  
 +    for (id = 0; id < NUM_THREADS;​ id++) { 
 +        arguments[id].first = id; 
 +        arguments[id].second = id * 2; 
 +        r = pthread_create(&​threads[id],​ NULL, f, (void *) &​arguments[id]);​ 
 +  
 +        if (r) { 
 +            printf("​Eroare la crearea thread-ului %ld\n"id); 
 +            exit(-1); 
 +        } 
 +    } 
 +  
 +    for (id = 0; id < NUM_THREADS;​ id++) { 
 +        r = pthread_join(threads[id]&​status);​ 
 +  
 +        if (r) { 
 +            printf("​Eroare la asteptarea thread-ului %ld\n",​ id); 
 +            exit(-1); 
 +        } 
 +    } 
 +  
 +    pthread_exit(NULL);​ 
 +
 +</​code>​ 
 +</​spoiler>​
  
-^   ''​MPI_Datatype '' ​ ^  Echivalentul ​din C/C++  ^ +Când dorim să paralelizăm operații efectuate pe arrays, fiecărui thread îi va reveni o bucată ​din array, pe acea bucată executând funcția atribuită lui (thread-ului).
-|   ''​MPI_INT'' ​   |     ''​int'' ​   | +
-|   ''​MPI_LONG'' ​  ​| ​    ''​long'' ​  | +
-|   ''​MPI_CHAR'' ​  ​| ​   ''​char'' ​   | +
-|   ''​MPI_FLOAT'' ​ |    ''​float'' ​  | +
-|  ''​MPI_DOUBLE'' ​ |    ''​double'' ​ |+
  
-===== Funcții ​de transmisie a datelor în MPI =====+Formula de împărțire:​ 
 +<code c> 
 +start_index ​id * (double) n / p 
 +end_index ​min(n, (id + 1) * (double) n / p)), unde id id-ul thread-ului,​ n dimensiunea array-ului, p numărul ​de threads 
 +</​code>​
  
-<​note>​ +===== Elemente de sincronizare ===== 
-**Convenție**:​ vom marca cu ↓ parametrii ​de tip input ai unei funcții și cu ↑ parametrii ​de tip output ​în cadrul prezentării funcțiilor de mai jos. +==== Mutex ==== 
-</​note>​+Un mutex (mutual exclusion) este folosit pentru a delimita și pentru a proteja o zonă critică, unde au loc, de regulă, operații de citire ​și de scriere. Un singur thread intră ​în zona critică (o rezervă pentru el - lock), unde se execută instrucțiuni, iar celelalte thread-uri așteaptă ca thread-ul curent să termine ​de executat instrucțiunile din zona critică. După ce thread-ul curent termină de executat instrucțiuni în zona critică, aceasta o eliberează (unlock) și următorul thread urmează aceiași pași.
  
-==== MPI_Send ==== +Pentru zonele critice în pthreads folosim ''​pthread_mutex_t'',​ care reprezintă un mutex, care asigură faptul ​că un singur thread accesează zona critică la un moment dat, thread-ul deținând lock-ul pe zona critică în momentul respectiv, și că celelalte thread-uri care nu au intrat încă în zona critică așteaptă eliberarea lock-ului de către thread-ul aflat în zona critică în acel moment.
-MPI_Send ​reprezintă ​funcția prin care un proces trimite date tre un alt proces. Semnătura funcției este următoarea:+
  
-''​int ​MPI_Send(voiddataint count, MPI_Datatype datatype, int destination,​ int tag, MPI_Comm communicator)''​, unde: +Funcții pentru mutex: 
-  * ''​data'' ​(- reprezintă datele trimise de la procesul sursă către procesul destinație +  * crearea unui mutex: ​''​int ​pthread_mutex_init(pthread_mutex_t ​*mutexconst pthread_mutexattr_t *attr);''​ 
-  * ''​count''​ (↓) - dimensiunea datelor transmise +  * lock pe mutex: ​''​int pthread_mutex_lock(pthread_mutex_t *mutex);''​ 
-  * ''​datatype'' ​(- tipul datelor transmise +  * unlock pe mutex: ​''​int pthread_mutex_unlock(pthread_mutex_t *mutex);''​ 
-  * ''​destination''​ (↓) - rangul / identificatorului procesului destinație,​ către care se trimit datele +  * distrugerea unui mutex: ​''​int pthread_mutex_destroy(pthread_mutex_t *mutex);''​
-  * ''​tag'' ​(- identificator al mesajului +
-  * ''​communicator''​ (↓) - comunicatorul în cadrul căruia se face trimiterea datelor între cele două procese +
-MPI_Send este o funcție blocantă. Mai precis, programul se blochează până când bufferul dat ca prim parametru poate fi refolosit, chiar dacă nu se execută acțiunea de primire a mesajului transmis de procesul curent (MPI_Recv). Dacă apare cazul în care procesul P1 trimite date (MPI_Send) la procesul P2, iar P2 nu are suficient loc în buffer-ul de recepție (buffer-ul nu are suficient loc liber sau este plin) atunci P1 se va bloca.+
  
-==== MPI_Recv ==== +Exemplu ​de folosire: 
-MPI_Recv reprezintă funcția prin care un proces primește date de la un alt proces. Semnătura funcției este următoarea:+<spoiler Click pentru exemplu>​ 
 +<code c> 
 +#include <​stdio.h>​ 
 +#include <​stdlib.h>​ 
 +#include <​pthread.h>​
  
-''​int MPI_Recv(void* data, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm communicator,​ MPI_Status* status)'',​ unde: +#define NUM_THREADS 2
-  * ''​data''​ (↑) - reprezintă datele primite de la procesul sursă de către procesul destinație +
-  * ''​count''​ (↓) - dimensiunea datelor primite +
-  * ''​datatype''​ (↓) - tipul datelor primite +
-  * ''​source''​ (↓) - rangul / identificatorului procesului sursă, care trimite datele +
-  * ''​tag''​ (↓) - identificator al mesajului +
-  * ''​communicator''​ (↓) - comunicatorul în cadrul căruia se face trimiterea datelor între cele două procese +
-  * ''​status''​ - conține date despre mesajul primit, ''​MPI_Status''​ fiind o structură ce conține informații despre mesajul primit (sursa, tag-ul mesajului, dimensiunea mesajului). Dacă nu dorim să ne folosim de datele despre mesajul primit, punem ''​MPI_STATUS_IGNORE'',​ prin care se ignoră status-ul mesajului. +
-În situația în care procesul P apelează funcția de MPI_Recv(), el se va bloca până va primi toate datele asteptate, astfel că dacă nu va primi nimic sau ceea ce primește este insuficient,​ P va rămâne blocat. Adică MPI_Recv() se termină doar în momentul în care buffer-ul a fost umplut cu datele așteptate.+
  
-Structura ''​MPI_Status''​ include următoarele câmpuri: +int a = 0; 
-  * ''​int count''​ - dimensiunea datelor primite +pthread_mutex_t mutex;
-  * ''​int MPI_SOURCE''​ - identificatorul procesului sursă, care trimis datele +
-  * ''​int MPI_TAG''​ - tag-ul mesajului primit+
  
 +void *f(void *arg)
 +{
 +    // facem lock pe mutex
 + pthread_mutex_lock(&​mutex);​
 +    // zona critica
 + a += 2;
 +    // facem unlock pe mutex
 + pthread_mutex_unlock(&​mutex);​
  
-MPI_Recv este o funcție blocantă, mai precis programul se poate bloca până când se execută acțiunea de trimitere a mesajului către procesul sursă.+ pthread_exit(NULL);​ 
 +}
  
-Un exemplu ​de program ​în care un proces trimite un mesaj către un alt proces:+int main(int argc, char *argv[]) { 
 + int i; 
 + void *status; 
 + pthread_t threads[NUM_THREADS];​ 
 + int arguments[NUM_THREADS];​ 
 + 
 +    // cream mutexul 
 + pthread_mutex_init(&​mutex,​ NULL); 
 + 
 + for (i = 0; i < NUM_THREADS;​ i++) { 
 + arguments[i] = i; 
 + pthread_create(&​threads[i],​ NULL, f, &​arguments[i]);​ 
 +
 + 
 + for (i = 0; i < NUM_THREADS;​ i++) { 
 + pthread_join(threads[i],​ &​status);​ 
 +
 + 
 +        // distrugem mutex-ul 
 + pthread_mutex_destroy(&​mutex);​ 
 + 
 + printf("​a = %d\n", a); 
 + 
 + return 0; 
 +
 +</​code>​ 
 +</​spoiler>​ 
 + 
 +==== Barieră ==== 
 +Bariera este folosită atunci când dorim să sincronizăm thread-urile încât să ajungă (să se sincronizeze) în același punct. Mai concret, ea asigură faptul că niciun thread, gestionat ​de barieră, nu trece mai departe de zona în care aceasta este amplasată decât atunci când toate thread-urile gestionate de barieră ajung în aceeași zonă. În pthreads folosim structura ''​pthread_barrier_t''​ pentru barieră. 
 + 
 +Funcții: 
 +  * crearea unei bariere: ''​int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned count);''​ 
 +  * așteptarea thread-urilor la barieră: ''​int pthread_barrier_wait(pthread_barrier_t *barrier);''​ 
 +  * distrugerea unei bariere: ''​int pthread_barrier_destroy(pthread_barrier_t *barrier);''​ 
 + 
 +Exemplu de folosire: 
 +<spoiler Click pentru exemplu>
 <code c> <code c>
-#include "​mpi.h"​ 
 #include <​stdio.h>​ #include <​stdio.h>​
 #include <​stdlib.h>​ #include <​stdlib.h>​
 +#include <​pthread.h>​
  
-int main (int argc, char *argv[]) +#define NUM_THREADS 8
-+
-    int  numtasks, rank, len; +
-    char hostname[MPI_MAX_PROCESSOR_NAME];​+
  
-    MPI_Init(&​argc,​ &​argv);​ +pthread_barrier_t barrier;
-    MPI_Comm_size(MPI_COMM_WORLD,​ &​numtasks);​ // Total number of processes. +
-    MPI_Comm_rank(MPI_COMM_WORLD,&​rank);​ // The current process ID / Rank. +
-    MPI_Get_processor_name(hostname,​ &len);+
  
-    srand(42); +void *f(void *arg) 
-    int random_num ​rand(); +{ 
-    printf("​Before ​send: process with rank %d has the number ​%d.\n", ​rank, +    int index *(int *arg; 
-            random_num);+  
 +    printf("​Before ​barrier - thread ​%d\n", index); 
 +    pthread_barrier_wait(&​barrier);​ 
 +    printf("​After barrier - thread ​%d\n", ​index);
  
-    ​if (rank == 0{ +    ​pthread_exit(NULL); 
-        ​MPI_Send(&​random_num1, MPI_INT, 1, 0, MPI_COMM_WORLD)+
-    } else + 
-        ​MPI_Status status+int main(int argcchar *argv[]) { 
-        ​MPI_Recv(&​random_num,​ 1, MPI_INT, 0, 0, MPI_COMM_WORLD,​ &status)+ int i
-        ​printf("​Process with rank %d, received %d with tag %d.\n",​ + void *status; 
-                rank, random_num, status.MPI_TAG); + pthread_t threads[NUM_THREADS];​ 
-    }+ int arguments[NUM_THREADS];
  
-    printf("After send: process with rank %d has the number %d.\n"rank, + pthread_barrier_init(&​barrierNULLNUM_THREADS);
-            random_num);+
  
-    MPI_Finalize();+ for (i = 0; i < NUM_THREADS;​ i++) { 
 + arguments[i] = i; 
 + pthread_create(&​threads[i],​ NULL, f, &​arguments[i]); 
 + }
  
 + for (i = 0; i < NUM_THREADS;​ i++) {
 + pthread_join(threads[i],​ &​status);​
 + }
 + pthread_barrier_destroy(&​barrier);​
 + return 0;
 } }
 </​code>​ </​code>​
 +</​spoiler>​
  
-<note important>​ +==== Semafor ==== 
-Când un proces X trimite un mesaj către un proces Y, tag-ul T al mesajului din MPI_Send, executat ​de procesul X, trebuie să fie același cu tag-ul mesajului ​din MPI_Recv, executat de procesul Y, deoarece procesul Y așteaptă un mesaj care are tag-ul T, altfel, dacă sunt tag-uri diferite, programul se va bloca. +Un semafor este un element de sincronizare,​ care reprezintă o generalizare a mutex-ului. Semaforul are un contor care este decrementat la intrarea unui thread în zona de cod critică și care e incrementat când thread-ul respectiv iese din zona critică (contorul nu poate fi negativ în pthreads).
-</​note>​+
  
-O ilustrație ​modului cum funcționează împreună funcțiile MPI_Send și MPI_Recv:+Pentru ​folosi semafoare în pthreads ne folosim de structura ''​sem_t'',​ pentru care trebuie să includem biblioteca ''​semaphore.h''​.
  
-{{ :app:​laboratoare:​send_recv.png?​700 |}}+Semafoarele POSIX sunt de două tipuri: 
 +  * cu nume - sincronizare între procese diferite 
 +  * fără nume - sincronizare între thread-urile din cadrul aceluiași proces
  
-Mai jos aveți un exemplu în care un proces trimite un întreg array de 100 de elemente către un alt proces:+Funcții: 
 +  * ''​int sem_init(sem_t *sem, int pshared, unsigned int value);''​ - inițiere semafor 
 +  * ''​int sem_destroy(sem_t *sem);''​ - distrugere semafor 
 +  * ''​int sem_wait(sem_t *sem);''​ - acquire 
 +  * ''​int sem_post(sem_t *sem);''​ - release 
 + 
 +Exemplu folosire - producer - consumer:
 <spoiler Click pentru exemplu> <spoiler Click pentru exemplu>
 <code c> <code c>
-#include "​mpi.h"​+#define _REENTRANT ​   1 
 #include <​stdio.h>​ #include <​stdio.h>​
 #include <​stdlib.h>​ #include <​stdlib.h>​
  
-int main (int argc, char *argv[]) +#include <​pthread.h>​ 
-+#include <​semaphore.h>​
-    int numtasks, rank, len; +
-    int size = 100; +
-    char hostname[MPI_MAX_PROCESSOR_NAME];​ +
-    int arr[size];+
  
-    MPI_Init(&​argc,​ &​argv);​ +#define NUM_THREADS ​   50 
-    MPI_Comm_size(MPI_COMM_WORLD,​ &​numtasks);​ +#define CONSUMER ​       0 
-    ​MPI_Comm_rank(MPI_COMM_WORLD,&​rank);​ +#define PRODUCER ​       1
-    ​MPI_Get_processor_name(hostname,​ &len);+
  
-    srand(42);​ +#define BUF_LEN ​        3
-    if (rank == 0) { +
-        for (int i = 0; i < size; i++) { +
-            arr[i] = i; +
-        }+
  
-        printf("​Process with rank [%d] has the following array:​\n",​ rank)+pthread_mutex_t mutex
-        for (int i = 0i < size; i++) { +sem_t full_sem    // semafor contor al elementelor pline 
-            printf("​%d ", arr[i]); +sem_t empty_sem   // semafor contor al elementelor goale
-        } +
-        ​printf("​\n"​);+
  
-        MPI_Send(arr,​ size, MPI_INT, 1, 1, MPI_COMM_WORLD);​ +char buffer[BUF_LEN]; 
-        printf("​Process with rank [%dsent the array.\n",​ rank)+int buf_cnt = 0;
-    } else { +
-        MPI_Status status; +
-        MPI_Recv(arr,​ size, MPI_INT, ​0, 1, MPI_COMM_WORLD,​ &​status);​ +
-        printf("​Process with rank [%d], received array with tag %d.\n",​ +
-                rank, status.MPI_TAG);+
  
-        printf("​Process with rank [%d] has the following array:​\n",​ rank); +void my_pthread_sleep ​(int millis) { 
-        for (int i = 0; i < size; i++) { +    ​struct timeval timeout;
-            ​printf("​%d ", arr[i]); +
-        } +
-        printf("​\n"​);​ +
-    }+
  
-    ​MPI_Finalize();+    ​timeout.tv_sec = millis / 1000; 
 +    timeout.tv_usec = (millis % 1000* 1000;
  
 +    select (0, NULL, NULL, NULL, &​timeout);​
 } }
-</​code>​ 
-</​spoiler>​ 
  
-==== MPI_Bcast ==== +void *producer_func ​(void *arg{ 
-MPI_Bcast reprezintă o funcție prin care un proces trimite un mesaj către toate procesele din comunicator ​(message broadcast), inclusiv lui însuși. +    ​sem_wait (&​empty_sem);​
-<note important>​În cadrul implementării MPI_Bcast sunt executate acțiunile de trimitere și de recepționare de mesaje, așadar nu trebuie să apelați MPI_Recv. +
-</​note>​Semnătura funcției este următoarea:​+
  
-''​int MPI_Bcast(void* data, int count, MPI_Datatype datatype, int root, MPI_Comm communicator)'',​ unde: +    pthread_mutex_lock ​(&mutex);
-  * ''​data''​ (↓ + ↑) - reprezintă datele care sunt transmise către toate procesele. Acest parametru este de tip input pentru procesul cu identificatorul ''​root''​ și este de tip output pentru restul proceselor. +
-  * ''​count''​ (↓) - dimensiunea datelor trimise +
-  * ''​datatype''​ (↓)- tipul datelor trimise +
-  * ''​root''​ (↓) - rangul / identificatorului procesului sursă, care trimite datele către toate procesele din comunicator,​ inclusiv lui însuși +
-  * ''​tag''​ (↓) - identificator al mesajului +
-  * ''​communicator''​ (↓) - comunicatorul în cadrul căruia se face trimiterea datelor către toate procesele din cadrul acestuia+
  
-O ilustrație care arată cum funcționează MPI_Bcast aveți mai jos:+    buffer[buf_cnt] = '​a';​ 
 +    buf_cnt++;​ 
 +    printf ("​Produs un element.\n"​);​
  
-{{ :​app:​laboratoare:​bcast.png?​500 |}}+    pthread_mutex_unlock (&​mutex);​
  
-==== MPI_Scatter ==== +    my_pthread_sleep (rand () % 1000);
-MPI_Scatter este o funcție prin care un proces împarte un array pe bucăți egale ca dimensiuni, unde fiecare bucată revine, în ordine, fiecărui proces, și le trimite tuturor proceselor din comunicator,​ inclusiv lui însuși.+
  
-Semnătura funcției este următoarea:​ +    sem_post ​(&​full_sem);
-''​int MPI_Scatter(void* send_data, int send_count, MPI_Datatype send_datatype,​ void* recv_data, int recv_count, MPI_Datatype recv_datatype,​ int root, MPI_Comm communicator)'',​ unde: +
-  * ''​send_data''​ (↓) - reprezintă datele care sunt împărțite și trimise către procesele din comunicator +
-  * ''​send_count''​ (↓) - reprezintă dimensiunea bucății care revine fiecărui proces (de regulă se pune ca fiind dimensiunea_totală / număr_de_procese). +
-  * ''​send_datatype''​ (↓) - tipul datelor trimise către procese +
-  * ''​recv_data''​ (↑) - reprezintă datele care sunt primite și stocate de către procese +
-  * ''​recv_count''​ (↓) - dimensiunea datelor primite (de regulă dimensiunea_totală / număr_de_procese) +
-  * ''​recv_datatype''​ (↓) - tipul datelor primite de către procese (de regulă este același cu send_datatype) +
-  * ''​root''​ (↓) - identificatorul procesului care împarte datele și care le trimite către procesele din comunicator,​ inclusiv lui însuși +
-  * ''​communicator''​ (↓) - comunicatorul din care fac parte procesele (de regulă ''​MPI_COMM_WORLD''​)+
  
-O ilustrație a modului cum funcționează MPI_Scatter:​+    return NULL; 
 +}
  
-{{ :​app:​laboratoare:​scatter.png?​500 |}} +void *consumer_func (void *arg) 
-==== MPI_Gather ==== +    ​sem_wait (&​full_sem);​
-MPI_Gather este o funcție care reprezintă inversul lui MPI_Scatter,​ în sensul că un proces primește elemente de la fiecare proces din comunicator,​ inclusiv de la el însuși, și le unifică într-o singură colecție.+
  
-Semnătura funcției este următoarea:​ +    pthread_mutex_lock ​(&mutex);
-''​int MPI_Gather(void* send_data, int send_count, MPI_Datatype send_datatype,​ void* recv_data, int recv_count, MPI_Datatype recv_datatype,​ int root, MPI_Comm communicator)'',​ unde: +
-  * ''​send_data''​ (↓) - reprezintă datele care trimise de fiecare proces către procesul cu id-ul root +
-  * ''​send_count''​ (↓) - reprezintă dimensiunea bucății trimisă de fiecare proces (de regulă se pune ca fiind dimensiunea_totală / număr_de_procese). +
-  * ''​send_datatype''​ (↓) - tipul datelor trimise de către procese +
-  * ''​recv_data''​ (↑) - reprezintă datele care sunt primite și stocate de către procesul root +
-  * ''​recv_count''​ (↓) - dimensiunea datelor primite (de regulă dimensiunea_totală / număr_de_procese) +
-  * ''​recv_datatype''​ (↓) - tipul datelor primite de către procesul root (de regulă este același cu send_datatype) +
-  * ''​root''​ (↓) - identificatorul procesului care primește datele (inclusiv de la el însuși) +
-  * ''​communicator''​ (↓) - comunicatorul din care fac parte procesele (de regulă ''​MPI_COMM_WORLD''​)+
  
-O ilustrare a modului cum funcționează MPI_Gather:+    buf_cnt--;​ 
 +    char elem = buffer[buf_cnt];​ 
 +    printf ("​Consumat un element%c\n", elem);
  
-{{ :​app:​laboratoare:​gather.png?​500 |}}+    pthread_mutex_unlock (&​mutex);​
  
-Mai jos aveți un exemplu ​de MPI_Scatter folosit împreună cu MPI_Gather:+    my_pthread_sleep (rand () % 1000); 
 + 
 +    sem_post (&​empty_sem);​ 
 + 
 +    return NULL; 
 +
 + 
 +int main () { 
 +    int i; 
 +    int type; 
 +    pthread_t tid_v[NUM_THREADS];​ 
 + 
 +    pthread_mutex_init (&​mutex,​ NULL); 
 + 
 +    sem_init (&​full_sem,​ 0, 0); 
 +    sem_init (&​empty_sem,​ 0, 3); 
 + 
 +    srand (time (NULL)); 
 +    for (i = 0; i < NUM_THREADS;​ i++) { 
 +        type = i % 2;   // ar funcționa șrand () % 2 în loc de i % 2? 
 +        if (type == CONSUMER) { 
 +            pthread_create (&​tid_v[i],​ NULL, consumer_func,​ NULL); 
 + } else { 
 +            pthread_create (&​tid_v[i],​ NULL, producer_func,​ NULL); 
 +        } 
 +    } 
 + 
 +    for (i = 0; i < NUM_THREADS;​ i++) { 
 +        pthread_join (tid_v[i], NULL); 
 +    } 
 + 
 +    pthread_mutex_destroy(&​mutex);​ 
 + 
 +    return 0; 
 +
 +</​code>​ 
 +</​spoiler>​ 
 + 
 +==== Variabile condiție ==== 
 +Variabilele condiție reprezintă o structură de sincronizare,​ care au asociat ​un mutex, și ele au un sistem ​de notificare a thread-urilor,​ astfel încât un thread să fie blocat până când apare o notificare de la alt thread. Pentru a putea folosi variabile condiție în pthreads ne folosim de structura ''​pthread_cond_t''​. 
 + 
 +Variabilele condiție sunt folosite pentru a bloca thread-ul curent (mutexul și semaforul blochează celelalte thread-uri). Acestea permit unui thread să se blocheze până când o condiție devine adevărată,​ moment când condiția este semnalată de thread că a devenit adevărată și thread-ul / thread-urile blocate de condiție își reiau activitatea. O variabilă condiție va avea mereu un mutex pentru a avea race condition, care apare când un thread 0 se pregătește să aștepte la variabila condiție și un thread 1 semnalează condiția înainte ca thread-ul 0 să se blocheze. 
 + 
 +Funcții: 
 +  * ''​int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);''​ - inițializare variabilă condiție 
 +  * ''​int pthread_cond_destroy(pthread_cond_t *cond);''​ - distrugere variabilă condiție 
 +  * ''​pthread_cond_t cond = PTHREAD_COND_INITIALIZER;''​ - inițializare statică a unei variabile condiție (atribute default, nu e nevoie de distrugere / eliberare) 
 +  * ''​int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);''​ - blocarea unui thread care așteaptă după o variabilă condiție 
 +  * ''​int pthread_cond_signal(pthread_cond_t *cond);''​ - deblocarea unui thread 
 +  * ''​int pthread_cond_broadcast(pthread_cond_t *cond);''​ - deblocarea tuturor thread-urilor blocate 
 +Exemplu folosire - producer - consumer:
 <spoiler Click pentru exemplu> <spoiler Click pentru exemplu>
 <code c> <code c>
 +#define _REENTRANT ​   1
 +
 #include <​stdio.h>​ #include <​stdio.h>​
 #include <​stdlib.h>​ #include <​stdlib.h>​
-#include <​mpi.h>​ 
  
-#define ROOT 0 +#include <​pthread.h>​
-#define CHUNK_SIZE 5 // numarul de elemente per proces+
  
-int main (int argc, char **argv) { +#define NUM_THREADS ​   50 
-    int rank, proc, a;+#define CONSUMER ​       0 
 +#define PRODUCER ​       1
  
-    int* arr; +#define BUF_LEN ​        3
-    int* process_arr;​ +
-    int* result_arr;​ +
-     +
-    MPI_Init(&​argc,​ &​argv);​ +
-     +
-    MPI_Comm_rank(MPI_COMM_WORLD,​ &​rank);​ +
-    MPI_Comm_size(MPI_COMM_WORLD,​ &proc);+
  
-    if (rank == ROOT) { +pthread_mutex_t mutex// folosit pentru incrementarea si decrementarea marimii buffer-ului 
-        arr = malloc (CHUNK_SIZE * proc * sizeof(int))+pthread_cond_t full_cond// cand buffer-ul este gol 
-        for (int i = 0i < proc * CHUNK_SIZE; ++i) { +pthread_cond_t empty_cond// cand buffer-ul este plin
-            ​arr[i] = 0; +
-        } +
-    }+
  
-    process_arr = malloc (CHUNK_SIZE * sizeof(int))+char buffer[BUF_LEN]
-    ​MPI_Scatter(arr,​ CHUNK_SIZE, MPI_INT, process_arr,​ CHUNK_SIZE, MPI_INT, ROOT, MPI_COMM_WORLD);+int buf_cnt = 0;
  
-    for (int i = 0; i < CHUNK_SIZE; i++) { +void my_pthread_sleep(int millis) { 
-        ​printf("​Before:​ rank [%d] - value = %d\n", rank, process_arr[i]); + struct timeval timeout;
-        process_arr[i] = i; +
-        printf("​After:​ rank [%d] - value = %d\n", rank, process_arr[i]);​ +
-    }+
  
-    if (rank == ROOT) { + timeout.tv_sec ​millis / 1000; 
-        ​result_arr ​malloc ​(CHUNK_SIZE ​proc * sizeof(int)); + timeout.tv_usec ​= (millis % 1000) 1000;
-    }+
  
-    MPI_Gather(process_arrCHUNK_SIZEMPI_INTresult_arrCHUNK_SIZE, MPI_INT, ROOT, MPI_COMM_WORLD);+ select ​(0NULLNULLNULL&​timeout); 
 +}
  
-    if (rank == ROOT) { +void *producer_func ​(void *arg) { 
-        for (int i = 0; i < CHUNK_SIZE ​proc; i++) { + pthread_mutex_lock ​(&mutex); 
-            ​printf("%d ", result_arr[i]); +  
-        } + // cat timp buffer-ul este plin, producatorul asteapta 
-        ​printf("​\n"​); + while ​(buf_cnt == BUF_LEN) { 
-    }+ pthread_cond_wait (&​full_cond,​ &mutex); 
 + }
  
-    if (rank == ROOT) { + buffer[buf_cnt] ​'​a';​ 
-        ​free(arr)+ buf_cnt++
-        free(result_arr); + printf ​("​Produs un element.\n"​);
-    }+
  
-    free(process_arr);+ pthread_cond_signal ​(&​empty_cond);​ 
 + my_pthread_sleep (rand () % 1000);
  
-    MPI_Finalize(); + pthread_mutex_unlock ​(&mutex); 
-    return ​0;+ 
 + return ​NULL;
 } }
  
 +void *consumer_func (void *arg) {
 + pthread_mutex_lock (&​mutex);​
 +
 + // cat timp buffer-ul este gol, consumatorul asteapta
 + while (buf_cnt == 0) {
 + pthread_cond_wait (&​empty_cond,​ &​mutex);​
 + }
 +
 + buf_cnt--;
 + char elem = buffer[buf_cnt];​
 + printf ("​Consumat un element: %c\n", elem);
 +
 + pthread_cond_signal (&​full_cond);​
 + my_pthread_sleep (rand () % 1000);
 +
 + pthread_mutex_unlock (&​mutex);​
 +
 + return NULL;
 +}
 +
 +int main() {
 + int i;
 + int type;
 + pthread_t tid_v[NUM_THREADS];​
 +
 + pthread_mutex_init (&​mutex,​ NULL);
 + pthread_cond_init (&​full_cond,​ NULL);
 + pthread_cond_init (&​empty_cond,​ NULL);
 +
 + srand (time (NULL));
 + for (i = 0; i < NUM_THREADS;​ i++) {
 + type = i % 2;   // ar funcționa și rand () % 2 în loc de i % 2?
 + if (type == CONSUMER) {
 + pthread_create (&​tid_v[i],​ NULL, consumer_func,​ NULL);
 + } else {
 + pthread_create (&​tid_v[i],​ NULL, producer_func,​ NULL);
 + }
 + }
 +
 + for (i = 0; i < NUM_THREADS;​ i++) {
 + pthread_join (tid_v[i], NULL);
 + }
 +
 + pthread_mutex_destroy(&​mutex);​
 +
 + return 0;
 +}
 </​code>​ </​code>​
 </​spoiler>​ </​spoiler>​
 +
 +===== Modele de programare =====
 +==== Boss worker ====
 +Modelul boss-worker este o versiune generalizată a modelului producer-consumer,​ unde avem un thread cu rolul de boss / master, care produce task-uri, și restul thread-urilor au rol de worker, ele având rolul de a executa task-urile produse de thread-ul boss.
 +
 +Task-urile sunt puse într-o coadă de către thread-ul boss și preluate de către thread-urile worker, care le execută.
 +
 +În general avem un singur thread boss, dar putem avea mai multe thread-uri de tip boss.
 +
 +Probleme ce pot apărea:
 +  * performanța când sunt multe task-uri executate prin refolosirea thread-urilor
 +  * resursele limitate pentru execuția thread-urilor
 +  * probleme de sincronizare
 +Boss worker se poate implementa folosind:
 +
 +  * două variabile condiționale
 +    * prima folosită de thread-urile worker să aștepte când coada este goală. Dacă nu e goală, workers semnalează thread-urilor boss și iau din coadă
 +    * a doua folosită de thread-urile boss să aștepte când coada este plină. Dacă nu e plină, boss semnalează thread-urilor worker și pune în coadă.
 +  * un mutex - protejează coada, variabilele condiționale și contoarele
 +  * o coadă - sincronizare când se pun / preiau date de către thread-uri
 +    * dimensiunea cozii poate să fie:
 +      * statică - sunt necesare două variabile condiție (una pentru boss, alta pentru workers)
 +      * dinamică - o necesară o variabilă condiție pentru workers (când coada e goală), dar are un dezavantaj pentru boss, mai precis acesta poate să fie supraîncărcat cu task-uri
 +  * trei contoare:
 +    * numărul de task-uri în coadă
 +    * numărul de thread-uri worker în așteptare
 +    * numărul de thread-uri boss în așteptare
 +
 +Thread-urile acționează într-o buclă continuă în ceea ce privește crearea și execuția task-urilor și ele nu își termină execuția când coada este goală, astfel trebuie să avem o modalitate de terminare a execuției thread-urilor boss și workers.
 +
 +Putem face terminarea thread-urilor în două moduri:
 +  * un task special de tip ”exit” pentru thread-urile worker, care se vor opri când primesc acest task (ordinea task-urilor să fie FIFO)
 +  * coada să aibă un flag de exit (ea trebuie să fie goală), setat de către thread-ul boss. Thread-urile worker se opresc când văd că coada este goală și flag-ul de exit setat pe true, iar apoi thread-ul boss se oprește
 +
 +==== Work crew ====
 +Work crew reprezintă un model de programare paralelă, unde avem un thread principal și N - 1 thread-uri, care sunt controlate de către thread-ul principal, care le creează. Cele N - 1 thread-uri sunt thread-uri de tip worker, care execută task-uri distribuite de către thread-ul principal, pe care, de asemenea, le execută, după ce thread-urile worker au terminat execuția lor.
 +
 +===== Probleme de sincronizare - opțional =====
 +
 +===== Exerciții =====
 +
 +1. Folosind la alegere modelul work-crew sau boss-worker,​ implementați însumarea elementelor dintr-o matrice, cu împărțirea matricei pe linii / coloane, fiecare thread cu liniile / coloanele sale, pe baza scheletului.
 +
 +
 +
app/laboratoare/04.1666536365.txt.gz · Last modified: 2022/10/23 17:46 by florin.mihalache
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0