This shows you the differences between two versions of the page.
app:laboratoare:05 [2022/10/26 21:20] florin.mihalache |
app:laboratoare:05 [2024/11/05 03:19] (current) alexandru.bala [MPI_Recv] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laboratorul 5 - pthreads ====== | + | ====== Laboratorul 5 - MPI ====== |
- | ===== Despre pthreads ===== | + | ===== Despre MPI ===== |
- | pthreads reprezintă o bibliotecă din C/C++, nativă Linux, prin care se pot implementa programe multithreaded. | + | 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. |
- | 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. | + | Un proces reprezintă un program aflat în execuție și se poate defini ca o unitate de bază care poate executa una sau mai multe sarcini în cadrul unui sistem de operare. Spre deosebire de thread-uri, un 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. |
- | ===== Implementarea unui program paralel în pthreads ===== | + | ===== Compilare și rulare ===== |
- | ==== Includere și compilare ==== | + | Î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: |
- | 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'': | + | * ''mpicc'', pentru lucrul în C |
- | <code bash> | + | * ''mpic++'', pentru lucrul în C++ |
- | gcc -o program program.c -lpthread | + | Î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. |
- | ./program | + | |
- | </code> | + | |
- | ==== Crearea și terminarea thread-urilor ==== | + | Exemplu: |
- | În pthreads, avem 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. | + | * compilare: |
+ | * C: ''mpicc hello.c -o hello'' | ||
+ | * C++: ''mpic++ hello.cpp -o hello'' | ||
+ | * rulare: ''mpirun -np 4 hello'' - rulare cu 4 procese | ||
+ | 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 o eroare cum ca nu aveți destule sloturi libere. Puteți elimina acea eroare adăugând parametrul ''--oversubscribe'' atunci când rulați ''mpirun''. | ||
- | Pentru a crea thread-uri în pthreads, folosim funcția ''pthread_create'': | + | ===== Instalare MPI ===== |
- | <code c> | + | Pentru a lucra cu MPI, trebuie să instalați biblioteca pentru MPI pe Linux, folosind următoarea comandă: ''sudo apt install openmpi-bin openmpi-common openmpi-doc libopenmpi-dev'' |
- | 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: | + | ===== Implementarea unui program distribuit în MPI ===== |
+ | Exemplu de program MPI - Hello World: | ||
<code c> | <code c> | ||
- | void *f(void *arg) { | + | #include "mpi.h" |
- | // 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 NUM_THREADS 2 | + | #define MASTER 0 |
- | void *f(void *arg) | + | int main (int argc, char *argv[]) { |
- | { | + | int numtasks, rank, len; |
- | long id = *(long*) arg; | + | char hostname[MPI_MAX_PROCESSOR_NAME]; |
- | printf("Hello World din thread-ul %ld!\n", id); | + | |
- | return NULL; | + | |
- | } | + | |
- | int main(int argc, char *argv[]) | + | MPI_Init(&argc, &argv); |
- | { | + | MPI_Comm_size(MPI_COMM_WORLD, &numtasks); |
- | pthread_t threads[NUM_THREADS]; | + | MPI_Comm_rank(MPI_COMM_WORLD,&rank); |
- | int r; | + | MPI_Get_processor_name(hostname, &len); |
- | long id; | + | if (rank == MASTER) |
- | void *status; | + | printf("MASTER: Number of MPI tasks is: %d\n",numtasks); |
- | long arguments[NUM_THREADS]; | + | else |
+ | printf("WORKER: Rank: %d\n",rank); | ||
- | for (id = 0; id < NUM_THREADS; id++) { | + | MPI_Finalize(); |
- | 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> | ||
- | Î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. | + | Un comunicator (''MPI_Comm'') reprezintă un grup de procese care comunică între ele. ''MPI_COMM_WORLD'' reprezintă comunicatorul default, din care fac parte toate procesele. |
- | Exemplu: | + | Funcții: |
- | <spoiler Click pentru 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. |
- | <code c> | + | * ''MPI_Comm_size'' - funcție care determină numărul de procese (numtasks) care rulează în cadrul comunicatorului (de regulă MPI_COMM_WORLD) |
- | #include <pthread.h> | + | * ''MPI_Comm_rank'' - funcție care determină identificatorul (rangul) procesului curent în cadrul comunicatorului. |
- | #include <stdio.h> | + | * ''MPI_Get_processor_name'' - determină numele procesorului |
- | #include <stdlib.h> | + | * ''MPI_Finalize'' - declanșează terminarea programului MPI |
- | + | ||
- | #define NUM_THREADS 8 | + | |
- | struct pair { | + | În cadrul schimbului de date între procese, este necesar mereu să precizăm tipul acestora. În MPI, se 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: |
- | int first, second; | + | |
- | }; | + | |
- | + | ||
- | void *f(void *arg) | + | |
- | { | + | |
- | struct pair info = *(struct pair*) arg; | + | |
- | printf("First = %d; second = %d\n", info.first, info.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> | + | |
- | 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_Datatype '' ^ Echivalentul din C/C++ ^ |
+ | | ''MPI_INT'' | ''int'' | | ||
+ | | ''MPI_LONG'' | ''long'' | | ||
+ | | ''MPI_CHAR'' | ''char'' | | ||
+ | | ''MPI_FLOAT'' | ''float'' | | ||
+ | | ''MPI_DOUBLE'' | ''double'' | | ||
- | Formula de împărțire: | + | ===== Funcții de transmisie a datelor în MPI ===== |
- | <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> | + | |
- | ===== Elemente de sincronizare ===== | + | <note> |
- | ==== Mutex ==== | + | **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. |
- | 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. | + | </note> |
- | 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 ==== |
+ | MPI_Send reprezintă funcția prin care un proces trimite date către un alt proces. Semnătura funcției este următoarea: | ||
- | Funcții pentru mutex: | + | ''int MPI_Send(void* data, int count, MPI_Datatype datatype, int destination, int tag, MPI_Comm communicator)'', unde: |
- | * crearea unui mutex: ''int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);'' | + | * ''data'' (↓) - reprezintă datele trimise de la procesul sursă către procesul destinație |
- | * lock pe mutex: ''int pthread_mutex_lock(pthread_mutex_t *mutex);'' | + | * ''count'' (↓) - dimensiunea datelor transmise |
- | * unlock pe mutex: ''int pthread_mutex_unlock(pthread_mutex_t *mutex);'' | + | * ''datatype'' (↓) - tipul datelor transmise |
- | * distrugerea unui mutex: ''int pthread_mutex_destroy(pthread_mutex_t *mutex);'' | + | * ''destination'' (↓) - rangul / identificatorului procesului destinație, către care se trimit datele |
+ | * ''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. | ||
- | Exemplu de folosire: | + | ==== MPI_Recv ==== |
- | <spoiler Click pentru exemplu> | + | MPI_Recv reprezintă funcția prin care un proces primește date de la un alt proces. Semnătura funcției este următoarea: |
- | <code c> | + | |
- | #include <stdio.h> | + | |
- | #include <stdlib.h> | + | |
- | #include <pthread.h> | + | |
- | #define NUM_THREADS 2 | + | ''int MPI_Recv(void* data, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm communicator, MPI_Status* status)'', unde: |
+ | * ''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. | ||
- | int a = 0; | + | Structura ''MPI_Status'' include următoarele câmpuri: |
- | pthread_mutex_t mutex; | + | * ''int count'' - dimensiunea datelor primite |
+ | * ''int MPI_SOURCE'' - identificatorul procesului sursă, care a 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); | ||
- | pthread_exit(NULL); | + | 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 de către procesul sursă. |
- | } | + | |
- | int main(int argc, char *argv[]) { | + | Un exemplu de program în care un proces trimite un mesaj către un alt proces: |
- | 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> | ||
- | #define NUM_THREADS 8 | + | int main (int argc, char *argv[]) |
+ | { | ||
+ | int numtasks, rank, len; | ||
+ | char hostname[MPI_MAX_PROCESSOR_NAME]; | ||
- | pthread_barrier_t barrier; | + | MPI_Init(&argc, &argv); |
+ | 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); | ||
- | void *f(void *arg) | + | srand(42); |
- | { | + | int random_num = rand(); |
- | int index = *(int *) arg; | + | printf("Before send: process with rank %d has the number %d.\n", rank, |
- | + | random_num); | |
- | printf("Before barrier - thread %d\n", index); | + | |
- | pthread_barrier_wait(&barrier); | + | |
- | printf("After barrier - thread %d\n", index); | + | |
- | pthread_exit(NULL); | + | if (rank == 0) { |
- | } | + | MPI_Send(&random_num, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); |
+ | } else { | ||
+ | MPI_Status status; | ||
+ | MPI_Recv(&random_num, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &status); | ||
+ | printf("Process with rank %d, received %d with tag %d.\n", | ||
+ | rank, random_num, status.MPI_TAG); | ||
+ | } | ||
- | int main(int argc, char *argv[]) { | + | printf("After send: process with rank %d has the number %d.\n", rank, |
- | int i; | + | random_num); |
- | void *status; | + | |
- | pthread_t threads[NUM_THREADS]; | + | |
- | int arguments[NUM_THREADS]; | + | |
- | pthread_barrier_init(&barrier, NULL, NUM_THREADS); | + | 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> | ||
- | ==== Semafor ==== | + | <note important> |
- | Un semafor este un element de sincronizare, care reprezintă o generalizare a mutex-ului. Semaforul are un contor care este incrementat la intrarea unui thread în zona de cod critică și care e decrementat când thread-ul respectiv iese din zona critică (contorul nu poate fi negativ în pthreads). | + | 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. |
+ | </note> | ||
- | Pentru a folosi semafoare în pthreads ne folosim de structura ''sem_t'', pentru care trebuie să includem biblioteca ''semaphore.h''. | + | O ilustrație a modului cum funcționează împreună funcțiile MPI_Send și MPI_Recv: |
- | Semafoarele POSIX sunt de două tipuri: | + | {{ :app:laboratoare:send_recv.png?700 |}} |
- | * cu nume - sincronizare între procese diferite | + | |
- | * fără nume - sincronizare între thread-urile din cadrul aceluiași proces | + | |
- | Funcții: | + | Mai jos aveți un exemplu în care un proces trimite un întreg array de 100 de elemente către un alt proces: |
- | * ''int sem_init(sem_t *sem, int pshared, unsigned int value);'' - inițiere semafor | + | |
- | * ''int sem_destroy(sem_t *sem);'' - distrugere semafor | + | |
- | * ''int sem_post(sem_t *sem);'' - acquire | + | |
- | * ''int sem_wait(sem_t *sem);'' - release | + | |
- | + | ||
- | Exemplu folosire - producer - consumer: | + | |
<spoiler Click pentru exemplu> | <spoiler Click pentru exemplu> | ||
<code c> | <code c> | ||
- | #define _REENTRANT 1 | + | #include "mpi.h" |
#include <stdio.h> | #include <stdio.h> | ||
#include <stdlib.h> | #include <stdlib.h> | ||
- | #include <pthread.h> | + | int main (int argc, char *argv[]) |
- | #include <semaphore.h> | + | { |
+ | int numtasks, rank, len; | ||
+ | int size = 100; | ||
+ | char hostname[MPI_MAX_PROCESSOR_NAME]; | ||
+ | int arr[size]; | ||
- | #define NUM_THREADS 50 | + | MPI_Init(&argc, &argv); |
- | #define CONSUMER 0 | + | MPI_Comm_size(MPI_COMM_WORLD, &numtasks); |
- | #define PRODUCER 1 | + | MPI_Comm_rank(MPI_COMM_WORLD,&rank); |
+ | MPI_Get_processor_name(hostname, &len); | ||
- | #define BUF_LEN 3 | + | srand(42); |
+ | if (rank == 0) { | ||
+ | for (int i = 0; i < size; i++) { | ||
+ | arr[i] = i; | ||
+ | } | ||
- | pthread_mutex_t mutex; | + | printf("Process with rank [%d] has the following array:\n", rank); |
- | sem_t full_sem; // semafor contor al elementelor pline | + | for (int i = 0; i < size; i++) { |
- | sem_t empty_sem; // semafor contor al elementelor goale | + | printf("%d ", arr[i]); |
+ | } | ||
+ | printf("\n"); | ||
- | char buffer[BUF_LEN]; | + | MPI_Send(arr, size, MPI_INT, 1, 1, MPI_COMM_WORLD); |
- | int buf_cnt = 0; | + | printf("Process with rank [%d] sent the array.\n", rank); |
+ | } 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); | ||
- | void my_pthread_sleep (int millis) { | + | printf("Process with rank [%d] has the following array:\n", rank); |
- | struct timeval timeout; | + | for (int i = 0; i < size; i++) { |
+ | printf("%d ", arr[i]); | ||
+ | } | ||
+ | printf("\n"); | ||
+ | } | ||
- | timeout.tv_sec = millis / 1000; | + | MPI_Finalize(); |
- | timeout.tv_usec = (millis % 1000) * 1000; | + | |
- | select (0, NULL, NULL, NULL, &timeout); | ||
} | } | ||
+ | </code> | ||
+ | </spoiler> | ||
- | void *producer_func (void *arg) { | + | ==== MPI_Bcast ==== |
- | sem_wait (&empty_sem); | + | 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. |
+ | <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: | ||
- | pthread_mutex_lock (&mutex); | + | ''int MPI_Bcast(void* data, int count, MPI_Datatype datatype, int root, MPI_Comm communicator)'', unde: |
+ | * ''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 | ||
- | buffer[buf_cnt] = 'a'; | + | O ilustrație care arată cum funcționează MPI_Bcast aveți mai jos: |
- | buf_cnt++; | + | |
- | printf ("Produs un element.\n"); | + | |
- | pthread_mutex_unlock (&mutex); | + | {{ :app:laboratoare:bcast.png?500 |}} |
- | my_pthread_sleep (rand () % 1000); | + | ==== MPI_Scatter ==== |
+ | 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. | ||
- | sem_post (&full_sem); | + | Semnătura funcției este următoarea: |
+ | ''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'') | ||
- | return NULL; | + | O ilustrație a modului cum funcționează MPI_Scatter: |
- | } | + | |
- | void *consumer_func (void *arg) { | + | {{ :app:laboratoare:scatter.png?500 |}} |
- | sem_wait (&full_sem); | + | ==== MPI_Gather ==== |
+ | 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. | ||
- | pthread_mutex_lock (&mutex); | + | Semnătura funcției este următoarea: |
+ | ''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'') | ||
- | buf_cnt--; | + | O ilustrare a modului cum funcționează MPI_Gather: |
- | char elem = buffer[buf_cnt]; | + | |
- | printf ("Consumat un element: %c\n", elem); | + | |
- | pthread_mutex_unlock (&mutex); | + | {{ :app:laboratoare:gather.png?500 |}} |
- | my_pthread_sleep (rand () % 1000); | + | Mai jos aveți un exemplu de MPI_Scatter folosit împreună cu MPI_Gather: |
- | + | ||
- | 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 = rand () % 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> | ||
- | #include <pthread.h> | + | #define ROOT 0 |
+ | #define CHUNK_SIZE 5 // numarul de elemente per proces | ||
- | #define NUM_THREADS 50 | + | int main (int argc, char **argv) { |
- | #define CONSUMER 0 | + | int rank, proc, a; |
- | #define PRODUCER 1 | + | |
- | #define BUF_LEN 3 | + | int* arr; |
+ | int* process_arr; | ||
+ | int* result_arr; | ||
+ | |||
+ | MPI_Init(&argc, &argv); | ||
+ | |||
+ | MPI_Comm_rank(MPI_COMM_WORLD, &rank); | ||
+ | MPI_Comm_size(MPI_COMM_WORLD, &proc); | ||
- | pthread_mutex_t mutex; // folosit pentru incrementarea si decrementarea marimii buffer-ului | + | if (rank == ROOT) { |
- | pthread_cond_t full_cond; // cand buffer-ul este gol | + | arr = malloc (CHUNK_SIZE * proc * sizeof(int)); |
- | pthread_cond_t empty_cond; // cand buffer-ul este plin | + | for (int i = 0; i < proc * CHUNK_SIZE; ++i) { |
+ | arr[i] = 0; | ||
+ | } | ||
+ | } | ||
- | char buffer[BUF_LEN]; | + | process_arr = malloc (CHUNK_SIZE * sizeof(int)); |
- | int buf_cnt = 0; | + | MPI_Scatter(arr, CHUNK_SIZE, MPI_INT, process_arr, CHUNK_SIZE, MPI_INT, ROOT, MPI_COMM_WORLD); |
- | void my_pthread_sleep(int millis) { | + | for (int i = 0; i < CHUNK_SIZE; i++) { |
- | struct timeval timeout; | + | printf("Before: rank [%d] - value = %d\n", rank, process_arr[i]); |
+ | process_arr[i] = i; | ||
+ | printf("After: rank [%d] - value = %d\n", rank, process_arr[i]); | ||
+ | } | ||
- | timeout.tv_sec = millis / 1000; | + | if (rank == ROOT) { |
- | timeout.tv_usec = (millis % 1000) * 1000; | + | result_arr = malloc (CHUNK_SIZE * proc * sizeof(int)); |
+ | } | ||
- | select (0, NULL, NULL, NULL, &timeout); | + | MPI_Gather(process_arr, CHUNK_SIZE, MPI_INT, result_arr, CHUNK_SIZE, MPI_INT, ROOT, MPI_COMM_WORLD); |
- | } | + | |
- | void *producer_func (void *arg) { | + | if (rank == ROOT) { |
- | pthread_mutex_lock (&mutex); | + | for (int i = 0; i < CHUNK_SIZE * proc; i++) { |
- | + | printf("%d ", result_arr[i]); | |
- | // cat timp buffer-ul este plin, producatorul asteapta | + | } |
- | while (buf_cnt == BUF_LEN) { | + | printf("\n"); |
- | pthread_cond_wait (&full_cond, &mutex); | + | } |
- | } | + | |
- | buffer[buf_cnt] = 'a'; | + | if (rank == ROOT) { |
- | buf_cnt++; | + | free(arr); |
- | printf ("Produs un element.\n"); | + | free(result_arr); |
+ | } | ||
- | pthread_cond_signal (&empty_cond); | + | free(process_arr); |
- | my_pthread_sleep (rand () % 1000); | + | |
- | pthread_mutex_unlock (&mutex); | + | MPI_Finalize(); |
- | + | 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 = rand () % 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 ===== | + | ===== Alte funcții ===== |
- | ==== Boss worker ==== | + | * Funcții nonblocante: ''MPI_Irecv'', ''MPI_Isend'', ''MPI_Ibcast'', ''MPI_Igather'', ''MPI_Iscatter'' etc. |
- | 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. | + | * Funcții sincrone: ''MPI_Ssend'', ''MPI_Issend'' |
+ | * ''MPI_Bsend'' - send cu buffer | ||
+ | * ''MPI_Barrier'' - barieră | ||
+ | * ''MPI_Reduce'' - operație distribuită de reduce pe arrays | ||
- | Task-urile sunt puse într-o coadă de către thread-ul boss și preluate de către thread-urile worker, care le execută. | + | Detalii despre aceste funcții puteți vedea [[https://ocw.cs.pub.ro/courses/apd/laboratoare/11 | aici]]. |
- | În general avem un singur thread boss, dar putem avea mai multe thread-uri de tip boss. | + | ===== Exerciții ===== |
+ | * Rulați exemplele de cod din [[https://github.com/cs-pub-ro/app-labs/tree/master/lab4/demo | folder-ul de demo]] din cadrul laboratorului. | ||
- | Probleme ce pot apărea: | + | * Scrieți un program ce adună un vector de elemente folosind MPI (pentru ușurință considerați că numărul de elemente e divizibil cu numărul de procese), folosind doar MPI_Send și MPI_Recv. Fiecare proces va calcula o suma intermediară, procesul master fiind cel care va calcula suma finală. |
- | * 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 | + | * Extindeți programul de calcul al sumei unui vector prin adăugarea unui coeficient la suma finală, fiecare proces va calcula suma parțială * coeficient: ''sum = sum * coeficient'' |
- | * 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ă | + | <note>Folosiți MPI_Bcast pentru a propaga valoarea coeficientului introdus de la tastatură.</note> |
- | * 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. | + | * Modificați programul de calcul al sumei unui vector astfel încât să folosiți MPI_Scatter și MPI_Gather pentru transferul informației (vector parțial și suma parțială). |
- | Putem face terminarea thread-urilor în două moduri: | + | ===== Resurse și referințe ===== |
- | * 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) | + | * [[https://ocw.cs.pub.ro/courses/apd/laboratoare/08 | Laboratorul 8 APD]] |
- | * 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 | + | * [[https://ocw.cs.pub.ro/courses/apd/laboratoare/11 | Laboratorul 11 APD]] |
- | + | * [[https://www.open-mpi.org | Open MPI]] | |
- | ==== Work crew ==== | + | * [[https://hpc-tutorials.llnl.gov/mpi/ | LLNL MPI Tutorial]] |
- | 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 ===== | + |