Sunteți pe pagina 1din 47

ALGORITMI PARALELI ȘI DISTRIBUIȚI

ÎNDRUMAR DE LABORATOR / SEMINAR


LISTA LUCRĂRILOR

1. Introducere în clasa Thread


2. Introducere în bibliotecile OpenMP
3. Implementare fire de execuție
4. Implementare semafoare
5. Implementarea şi testare algoritm de sortare paralelă
6. Implementarea unor algoritmi din metodele numerice.
7. Testarea algoritmilor paraleli
LABORATORUL NR.1
Introducere în clasa Thread

Scop:
1. Înțelegrea noțiunilor privind implemementarea accesului concurențial prin clasa Thread

Consideraţii teoretice:
Sistemele multiprocesor: calculele sunt repartizate mai multor procesoare fizice. În aceste sisteme
multiprocesor, comunicarea între procesoare are loc fie prin zone de memorie comune (partajate), fie prin
canale.

Algoritmi de calcul paralel (care determină soluţia unei probleme prin descompunerea ei în
subprobleme independente, rezolvabile în paralel pe procesoare distincte); de exemplu este evident că suma a
doi vectori, calculată prin:
for i:=1 to n do
c[i]:=a[i]+b[i]
se efectuează mai rapid dacă avem la dispoziţie n procesoare, fiecare capabil să efectueze o adunare.
Programare secvențială VS Programare concurentă
Programarea concurentă rezervă o serie de surprize informaticianului obişnuit cu programarea secvenţială.
Prezentăm în continuare câteva dintre ele.
1) În programarea secvenţială, următoarea secvenţă de instrucţiuni:
i:=1; i:=2
este echivalentă cu a doua instrucţiune.
În programarea concurentă secvenţa de mai sus nu are nimic redundant. Într-adevăr, în intervalul de timp
dintre executarea celor două instrucţiuni (în cadrul aceluiaşi proces) este posibil ca celelalte procese să execute
diferite instrucţiuni care să folosească efectiv faptul că un anumit interval de timp (asupra lungimii căruia nu
putem face nici o ipoteză) valoarea lui i este egală cu 1.
2) Efectul instrucţiunii:
if i=1 then j:=j+i
în cazul în care valoarea lui i era 1 la începutul executării acestei instrucţiuni condiţionale nu constă neapărat în
mărirea cu o unitate a valorii lui j, deoarece între momentul efectuării comparaţiei şi momentul efectuării
atribuirii valoarea lui i poate fi eventual modificată de un alt proces. Analog, dacă într-un proces apare
secvenţa de instrucţiuni:
i:=1; if i=1 then instr
atunci pe de o parte nu este neapărat adevărat că este îndeplinită condiţia i=1 din instruţiunea if, iar pe de altă
parte nu este neapărat adevărat că în momentul executării instrucţiunii instr valoarea lui i este egală cu 1; în
schimb afirmaţia valoarea lui i a fost egală cu 1 la un moment de timp anterior" este adevărată şi poate fi utilă
(de exemplu în a demonstra că procesul are o anumită evoluţie).
Noţiunea de secţiune critică

Problema rezervării biletelor. Reamintim că problema constă în simularea activităţii mai multor
terminale conectate la un calculator central, terminale de la care se pot face rezervări de bilete pentru un
anumit spectacol; bineînţeles că trebuie evitată vânzarea a două bilete pentru acelaşi loc.
Considerăm următoarea modalitate prin care un proces face rezervarea, în ipoteza că mai există locuri
libere şi că fiecare persoană ce accesează un terminal are anumite preferinţe pentru locurile din sală, dar este
hotărâtă să cumpere un bilet:
repeat
rez:=false; { soseşte un nou client }
repeat { până când clientul cere un loc liber }
expune planul sălii
read(loc_client); { citeşte locul cerut de client }
if sala[loc_client]=1 { locul este liber }
then begin
sala[loc_client]:=0; rez:=true;
eliberează bilet
end
else write('Alta optiune: ')
until rez
forever

Este uşor de observat că această modalitate este incorectă.


Vom numi secţiune critică o secvenţă de instrucţiuni a căror executare trebuie să se supună
următoarei reguli: în momentul în care un proces P începe executarea primei instrucţiuni din secvenţă, toate
celelalte procese îşi întrerup temporar activitatea până la terminarea executării ultimei instrucţiuni din
secvenţă de către procesul P. În aceste condiţii spunem că are loc o excludere reciprocă (cel mult un proces se
poate afla într-o secţiune critică).
Să observăm că dacă în codul de mai sus ataşat instrucţiunii if primele patru acţiuni (linii de cod) ar
forma o secţiune critică, atunci modalitatea de rezervare de mai sus ar fi corectă.
Crearea firelor de executare

 Clasa Thread

Un prim mod de a crea şi lansa în executare fire este oferit de clasa Thread din pachetul java.lang:
public class Thread extends Object implements Runnable

Crearea firului se face astfel:


Thread T = new Thread();
După creare, un fir este lansat în executare prin invocarea metodei run. Aceasta este o metodă a
clasei Thread şi nu prevede vreo acţiune; de aceea ea trebuie redefinită conform dorinţelor programatorului.
Când metoda run se termină, se termină şi firul care a invocat-o.
Lansarea în executare a unui fir face ca acesta să fie executat concurent cu firul care l-a lansat. De
aceea invocăm metoda run prin start.
Exemplul 1.

class Tip extends Thread {


int k; boolean b=true;

public void run() {


while(b) IO.write(" " + k++);
IO.writeln("Thread");
}

public void terminare() { b=false; }


}

class P1 {
public static void main(String[] w)
Tip Ob = new Tip(); Ob.start();
while ( IO.readch() != '\n' );
Ob.terminare(); IO.writeln("main");
}
}

Dacă în loc de Ob.start() am fi folosit Ob.run(), programul ar rula la infinit (programul principal
aşteaptă ca obiectul Ob să-şi termine acţiunea) !!!
 Interfaţa Runnable

O a doua modalitate de a crea şi lansa în executare fire este de a folosi interfaţa Runnable, ce apare
în pachetul java.lang şi anunţă numai metoda run.

class Execute implements Runnable {


...
public void run() { . . . }
...
}

class C {
...
Execute Ob = new Execute(...);
Thread T = new Thread(Ob); // firul este creat
T.start(); // firul este pornit
...
}
Exemplul 2.

class Execute implements Runnable {


int k; boolean b=true;
public void run() {
while (b) IO.write(" " + k++);
IO.writeln("Thread");
}
public void terminate() { b = false; }
}

class RunThread {
public static void main(String[] arg) {
Execute Ob = new Execute();
Thread T = new Thread(Ob);
T.start();
while ( IO.readch() != '\n');
Ob.terminate(); IO.writeln("main");
}
}

Desfăşurarea lucrării:
1. Se copiază exemplele prezentate şi se vor analiza rezultatele prin repetarea execuției
2. Se vor extinde instanțierile existente şi se va face o analiză a rezultatului
LABORATORUL NR.2
Introducere în bibliotecile OpenMP

Scop:
1.

Consideraţii teoretice:

Desfăşurarea lucrării:
Pentru compilarea unui program ce utilizează OpenMP cu gcc sai g++ vom utiza opțiunea -fopenmp atât
pentru compilare cât şi pentru linkeditare.
În momentul instalării MinGW /TDM se va selecta opțiunea OpenMP pentru aducerea acestor biblioteci.
Pentru a executa in paralel 2 portiuni de cod (se vor aduna 2 matrici) folosind OpenMP, se folosesc 2 blocuri
omp section, aceste 2 blocuri fiind cuprinse intr-un bloc omp parallel sections.
Un program OpenMP incepe cu un proces singular (thread master) -> secvential -> constructie de regiune
paralela (FORK) -> mai multe thread-uri in paralel -> JOIN -> thread-ul master etc.
Numarul de thread-uri : se poate modifica dinamic.

Structura unui program OpenMP :

#include <opm.h>
void main ( )
{
int var1, var2, var3;
..... cod secvential .....
//incepe sectiunea paralela => FORK
#pragma omp parallel private (var1, var2) shared (var3)
{
..... sectiune paralela executata de toate thread-urile .....
//toate thread-urile => JOIN => thread master
}
..... reia cod secvential .....
}

Formatul unei directive

#pragma omp nume_directiva [clauza, clauza, ...] newline

unde clauzele pot fi plasate in orice ordine si se pot chiar repeta. Liniile directive lungi se pot continua pe
randul urmator cu ’\’.

Directiva pentru regiune paralela


Regiunea paralela = bloc de cod care se va executa de mai multe thread-uri.

La o directivă parallel un thread crează un set de thread-uri si devine masterul setului (cu numărul
de thread 0 în cadrul setului).
Numarul de thread-uri : omp_set_num_threads() sau cu variabila de mediu OMP_NUM_THREADS.
Clauza if evalueaza expresia scalara: daca expresia ≠ 0 (adevarat) se creaza setul de thread-uri, iar
daca expresia = 0 (fals) regiunea este executata numai de thread-ul master.
La sfarsitul sectiunii paralele numai thread-ul master isi continua executia.
Exemplu: toate thread-urile executa codul corespunzator sectiunii paralele.

> Directiva pentru partajarea lucrului


Aceasta directiva imparte regiunea de cod intre thread-urile membre, fara insa sa lanseze noi
thread-uri.
Constructiile de partajare a lucrului sunt:
-for;
-sections;
-single.
O constructie de partajare a lucrului trebuie inclusa dinamic intr-o regiune paralela pentru a fi executata in
paralel.
> Directiva for
Partajeaza iteratiile unei bucle for intre thread-urile setului (reprezinta un tip de paralelism de date):
Forma generala a directivei for este:

Clauza schedule, in functie de tip, descrie cum se impart iteratiile buclei for intre thread-urile din set:
-static: iteratiile se impart in sectiuni de dimensiune chunk si asignate static thread-urilor (daca nu
se specifica chunk, iteratiile se impart in mod egal).
-dynamic: iteratiile se impart in sectiuni de dimensiune chunk si se asigneaza dinamic thread-urilor.
Cand un thread termina bucata sa, acesta este asignat dinamic la o alta bucata din totalul de iteratii
(valoarea implicita chunk=1).
-guided: dimensiunea bucatii este redusa exponential cu fiecare bucata repartizata din totalul
iteratiilor. Dimensiunea bucatii reprezinta numarul minim de iteratii de repartizat de fiecare data (implicit
chunk=1).
-runtime: decizia de repartizare este amanata pana in timpul executiei, fiind determinata de
variabila de mediu OMP_SCHEDULE (nu se specifica dimensiune chunk).

Clauza ordered trebuie sa fie prezenta cand sunt incluse in directiva for si directive ordered.
Clauza nowait indica faptul ca thread-ul nu se sincronizeaza la sfarsitul buclei paralele.
Exemplu: program pentru adunarea a doi vectori.

Directiva sections
Imparte lucrul in sectiuni discrete separate, fiecare sectiune fiind executata de un thread, pentru
implementarea unui paralelism functional.
Forma generala a directivei sections este:

Fiecare sectiune „section” se executa o singura data de catre un singur thread, sectiuni diferite vor fi
executate de thread-uri diferite. Exista bariera implicita la sfarsit, numai daca nu se utilizeaza „nowait”.
Exemplu: adunarea a doi vectori, primele n/2 iteratii fiind distribuite primului thread, iar restul la un al
doilea thread.
Directiva single
Aceasta directiva serializeaza o sectiune de cod. Codul este executat de un singur thread. Forma generala:

Directiva parallel for


Se pot combina directivele de sectiune paralela si de partajarea a lucrului cu ajutorul directivei
parallel for, prin care se specifica o regiune paralela care contine o singura directiva for. Forma generala:

Exemplu: iteratiile sunt repartizate in blocuri de dimensiuni egale la toate thread-urile din set.
Exemplu:
Avem 4 matrici, A,B,C,D pe care vrem sa le adunam astfel: X=A+B, Y=C+D si la final Z=X+Y.
Pentru ca acest program sa se execute intr-un timp cat mai scurt, vom paraleliza cele 2 operatii de adunare:
X=A+B si Y=C+D.
#pragma omp parallel sections private(i,j) shared(A,B,C,D,X,Y)
{
#pragma omp section
for(i=0; i<n; ++i)
for(j=0; j<n; ++j)
X[i][j] = A[i][j]+B[i][j];

#pragma omp section


for(i=0; i<n; ++i)
for(j=0; j<n; ++j)
Y[i][j] = C[i][j]+D[i][j];
}
iar la final adunam cele 2 rezultate:
for(i=0; i<n; ++i)
for(j=0; j<n; ++j)
Z[i][j] = X[i][j]+Y[i][j];

Inmultirea a doua matrici.


Presupunem a si b de tip Mn*n .
Declaram a, b si c de tip double si c=a*b.
Algoritmul secvential de inmultire a doua matrici este:
1. for [ i = 0 to n-1 ]
2. for [j = 0 to n-1 ]
3. { c[i, j] = 0.0 ;
4. for [k = 0 to n-1]
5. c[i,j] = c[i, j] + a[i, k] * b[k, j];
6. }
Definitie: doua operatii se pot executa in paralel daca sunt independente.
Fie ,, o " o operatie.
RS(o) este multimea variabilelor utilizate in o si nemodificate de o.
WS(o) este multimea variabilelor modificate de o (si eventual utilizate).
Doua operatii o1 si o2 sunt independente daca:
a) WS(o1) ? WS(o2) = ?
b) RS(o1) ? RS(o2) = ?
Ex.
Instructiunea o : x = y + x + z
RS(o) = {y , z}
WS(o) = {x}
Obs. Variabilele utilizate sunt cele care apar in partea dreapta a atribuirii.
Revenind la exemplul de mai sus, cel cu inmultirea matricelor, observam ca pentru linia 5 de cod avem:
RS(5) = {a[i, k] , b[k, j]} iar WS(5) = {c[i, j]}
Notatie: ,, co ,, pentru a simula executia paralela. co - proces
co [k=0 to n-1]
c[i, j] = c[i, j] + a[i, k]*b[k, j]
oc
Insa se poate observa ca operatiile de la randul 5 nu sunt independente deoarece:
WS(5, i0) ? WS(5, j0) = {c[i, j]}
Atunci operatiile care pot fi paralelizate sunt:
co [ i = 0 to n-1 ] (se vor calcula in paralel liniile matricii c)
for [j = 0 to n-1 ]
{ c[i, j] = 0.0 ;
for [k = 0 to n-1]
c[i,j] = c[i, j] + a[i, k] * b[k, j];
}
Adica: procesul 0 va calcula linia 0, ... procesul i va calcula linai i ,...procesul n-1 va calcula linia n-1.
Sau presupunem ca avem n2 procese care fiecare calculeaza un element al matricii c.
co [ i = 0 to n-1 , j = 0 to n-1 ]
{ c[i, j] = 0.0 ;
for [k = 0 to n-1]
c[i,j] = c[i, j] + a[i, k] * b[k, j];
}

Un alt exemplu este reprezentat de înmulțirea a două matrici:


/*
* Sample program to test runtime of simple matrix multiply
* with and without OpenMP on gcc-4.3.3-tdm1 (mingw)
* (c) 2009, Rajorshi Biswas
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <omp.h>
int main()
{
int i,j,k,n; //n=matrix dimensions (n*n)
int num_threads; // number of threads
int t1, t2;
double **A; // matrix 1 = A
double **B; // matrix 2 = B
double **M; // matrix 3 = A * B
double temp;
double start, end;
printf ("matmul - matrix multiplicationn");
printf("Threads: ");
scanf("%d",&num_threads);
printf("N: ");
scanf("%d",&n);
// simple OpenMP construction
#pragma omp parallel
{
t2 = omp_get_num_threads();
t1 = omp_get_thread_num();
printf("Hello from thread %d of %dn", t1, t2);
}
// allocate dynamic memory for the each matrix
A = (double **)malloc( sizeof(double*) * n);
B = (double **)malloc( sizeof(double*) * n);
M = (double **)malloc( sizeof(double*) * n);
for(i=0; i<n; ++i) {
A[i] = malloc( sizeof(double) * n );
B[i] = malloc( sizeof(double) * n );
M[i] = malloc( sizeof(double) * n );
}
// populating array with random values
srand( time(NULL) );
for(i=0; i<n; ++i) {
for(j=0; j<n; ++j) {
A[i][j] = (1.0 * rand() / RAND_MAX);
B[i][j] = (1.0 * rand() / RAND_MAX);
}
}
// calculate the product of two matrices without OpenMP
printf("Calculation without OpenMP...");
fflush(stdout);
/*************************************************/
start = omp_get_wtime();
for(i=0; i<n; ++i) {
for(j=0; j<n; ++j) {
temp = 0;
for(k=0; k<n; ++k) {
temp += A[i][k] * B[k][j];
}
M[i][j] = temp;
}
}
end = omp_get_wtime();
/*************************************************/
printf(" took %f seconds.n", end-start);
printf("Calculation without OpenMP... %d threads...", num_threads);
fflush(stdout);
/*************************************************/
omp_set_num_threads(num_threads);
start = omp_get_wtime();
#pragma omp parallel for private(i, j, k, temp) shared(A, B, M)
for(i=0; i<n; ++i) {
for(j=0; j<n; ++j) {
temp = 0;
for(k=0; k<n; ++k) {
temp += A[i][k] * B[k][j];
}
M[i][j] = temp;
}
}
end = omp_get_wtime();
/*************************************************/
printf(" took %f seconds.n", end-start);
return 1;
}

Desfăşurarea lucrării:
1. Se copiază exemplele prezentate şi se vor analiza rezultatele prin repetarea execuției
2. Se vor extinde instanțierile existente şi se va face o analiză a rezultatului
LABORATORUL NR.3
Implementare fire de execuție

Scop: Înșelegerea și implementarea firelor de execuţie

Consideraţii teoretice:
Fire de executare
Considerăm că lucrăm pe un calculator cu np 1 procesoare. Ne îndreptăm atenția supra modului în
care putem lansa în executare mai multe fire de executare (procese).
Un fir de executare este un program secvențial, ce poate fi executat concurent cu alte fire.

Crearea firelor de executare

Clasa Thread

Un prim mod de a crea şi lansa în executare fire este oferit de clasa Thread din pachetul java.lang:
public class Thread extends Object implements Runnable

Crearea firului se face astfel:


Thread T = new Thread();
După creare, un fir este lansat în executare prin invocarea metodei run. Aceasta este o metodă a
clasei Thread şi nu prevede vreo acțiune; de aceea ea trebuie redefinită conform dorințelor programatorului.
Când metoda run se termină, se termină şi firul care a invocat-o.
Lansarea în executare a unui fir face ca acesta să fie executat concurent cu firul care l-a lansat. De aceea
invocăm metoda run prin start.
Exemplul 1.
class Tip extends Thread {
int k; boolean b=true;

public void run() {


while(b) IO.write(" " + k++);
IO.writeln("Thread");
}

public void terminare() { b=false; }


}

class P1 {
public static void main(String[] w)
Tip Ob = new Tip(); Ob.start();
while ( IO.readch() != '\n' );
Ob.terminare(); IO.writeln("main");
}
}
Dacă în loc de Ob.start() am fi folosit Ob.run(), programul ar rula la infinit (programul principal
aşteaptă ca obiectul Ob să-şi termine acțiunea) !!!
Interfața Runnable
O a doua modalitate de a crea şi lansa în executare fire este de a folosi interfața Runnable, ce apare în
pachetul java.lang şi anunță numai metoda run.
class Execute implements Runnable {
. . .
public void run() { . . . }
. . .
}

class C {
. . .
Execute Ob = new Execute(...);
Thread T = new Thread(Ob); // firul este creat
T.start(); // firul este pornit
. . .
}
Exemplul 2.
class Execute implements Runnable {
int k; boolean b=true;
public void run() {
while (b) IO.write(" " + k++);
IO.writeln("Thread");
}
public void terminate() { b = false; }
}

class RunThread {
public static void main(String[] arg) {
Execute Ob = new Execute();
Thread T = new Thread(Ob);
T.start();
while ( IO.readch() != '\n');
Ob.terminate(); IO.writeln("main");
}
}

Modelul aleator
Fiecare proces execută repetat următoarele acțiuni:
1) alege aleator un proces "liber";
2) execută un timp aleator instrucțiuni ale sale.

Modelul este independent de numărul real de procesoare.

Un prim mod de sincronizare

Dacă dintr-un fir am lansat alt fir prin intermediul obiectului Ob şi dorim să aşteptăm ca acesta să-şi termine
acțiunea, vom invoca metoda join a clasei Thread.

Exemplul 3.
class Tip extends Thread {
static int k=1;
public void run() {
for (int i=0; i<500; i++) IO.write(k+" ");
}
}

class P2 {
public static void main (String[] arg)
throws InterruptedException {
Tip Ob = new Tip(); Ob.start();
Thread.sleep(50); // executarea este amanata 50 milisec.
Ob.k = 2; Ob.join();
IO.writeln("***");
}
}
Observație. Firul care lansează un nou fir nu are drepturi suplimentare: de exemplu se poate
termina înaintea celui pe cate îl lansează.

Metoda arborelui binar


Această metodă are o largă aplicabilitate în calculul paralel.
Dorim să calculăm suma elementelor tabloului a=(a0,...,an-1).
Putem presupune că n=2m pentru un anumit m, adăugând zerouri la dreapta.
Primul pas constă în dublarea lungimii vectorului, prin mutarea elementelor pe pozițiile an,..,a2n-1.
Acum putem considera că elementele sunt noduri ale unui arbore binar complet.
Observații:
- vârfurile apar pe nivelurile 0,1, ..., m-1;
- vârfurile de pe nivelurile 0,1, …,m-2 au exact 2 fii, iar cele de pe nivelul m-1 sunt frunze;
- vârfurile de pe nivelul i sunt 2i..2i+1-1; fiii vârfului i sunt 2i şi 2i+1.
Al doilea pas constă în traversarea bottom-up a arborelui. Pe fiecare nivel, calculele se fac în paralel.

Pasul 1.
for i=0,n-1 in parallel do
an+i  ai {1}
endfor

Pasul 2.

for k=m-1,0,-1 do
for i=2k,2k+1-1 in parallel {2}
ai  a2i + a2i+1
endfor
endfor

Teoretic, folosim n procesoare. Timpul este de ordinul O(m) = O(log n), deoarece ciclul for interior
necesită un timp constant şi este executat de m ori.

Programul Java corespunzător este următorul:


class Type1 extends Thread {
static int n; int i;
public Type1(int i) { this.i = i; }
public void run() { BinTree.a[n+i] = BinTree.a[i]; }
}

class Type2 extends Thread {


int i;
public Type2(int i) { this.i = i; }
public void run() {
BinTree.a[i] = BinTree.a[2*i]+BinTree.a[2*i+1];
}
}

class BinTree {
static int[] a = new int[32]; static int n;
public static void main(String[] arg) {
int m=0, i;
IO.write("m= "); m = (int) IO.read(); //m<=5
int n=1; for (i=0; i<m; i++) n *= 2;
IO.write("Numbers : ");
for (i=0; i<n; i++) a[i] = (int) IO.read();
// ------------------------------------------

Type1[] Ob1 = new Type1[n]; Type1.n = n;


for (i=0; i<n; i++) Ob1[i] = new Type1(i);
for (i=0; i<n; i++) Ob1[i].start();
try { for (i=0; i<n; i++) Ob1[i].join(); }
catch (InterruptedException e) { }
// ------------------------------------------

Type2[] Ob2 = new Type2[n];


int p = n/2, u = n;
for (int k=m-1; k>=0; k--) {
for (i=p; i<u; i++) Ob2[i] = new Type2(i);
for (i=p; i<u; i++) Ob2[i].start();
try { for (i=p; i<u; i++) Ob2[i].join(); }
catch(InterruptedException e) { }
for (i=p; i<u; i++) IO.write(a[i]+" ");
IO.writeln();
p = p/2; u = u/2;
}
IO.writeln("Suma = "+a[1]);
}
}

Problema grădinilor ornamentale (atribuirii multiple)


class C {
static int total; int temp;

synchronized void compute() {


temp=total; temp++; total=temp;
}
}

class Type extends Thread {


static C Ob; int i;
Type(int i) { this.i=i; }
void delay() {
try { Thread.sleep( (int) (100 * Math.random()) ); }
catch(InterruptedException e) { }
}

public void run() {


for (int j=1; j<=10; j++) {
delay(); Ob.compute(); IO.write(" "+i);
}
}
}

class Garden {
public static void main(String[] s) {
Type.Ob = new C();
Type[] T = new Type[5];
for (int i=0; i<5; i++) T[i] = new Type(i);
for (int i=0; i<5; i++) T[i].start();
try { for (int i=4; i>=0; i--) T[i].join(); }
catch (InterruptedException e) { }
IO.writeln(); IO.writeln("Total = " + C.total);
}

Desfăşurarea lucrării:
1. Se copiază exemplele prezentate şi se vor analiza rezultatele prin repetarea execuției
2. Se vor extinde instanțierile existente şi se va face o analiză a rezultatului
LABORATORUL NR.4
Implementare semafoare

Scop:
1. Înțelegerea conceptului de semafor
2. Implementarea mecanismului privind semafoarele

Consideraţii teoretice:
Semafoare
Noţiunea de semafor

Una dintre modalităţile de a rezolva mai simplu problema excluderii reciproce, precum şi alte
probleme din programarea concurentă, este utilizarea semafoarelor.
Noţiunea de semafor a fost introdusă în 1968 de către Dijkstra.
Semaforul este un tip de date, caracterizat deci prin valorile pe care le poate lua şi operaţiile în care
poate interveni.
O variabilă de tip semafor (general) poate lua ca valori doar numere naturale. Dacă valorile permise
pot fi numai 0 sau 1, spunem că este vorba de un semafor binar.
Conceptual, unui semafor s îi este asociată o mulţime de blocare Bs, iniţial vidă.

Operaţiile definitorii pentru semafoare sunt următoarele:


1) wait(s) : dacă valoarea semaforului s este diferită de zero (pozitivă), atunci se micşorează cu o unitate
această valoare şi se trece mai departe; în caz contrar procesul care execută această operaţie este blocat în Bs;
2) signal(s) : dacă există procese blocate de semaforul s, unul dintre ele este deblocat şi trecut în starea
"gata de executare"; în caz contrar valoarea lui s este mărită cu o unitate.
Operaţiile wait şi respectiv signal mai sunt notate cu P şi respectiv V (iniţialele cuvintelor din limba
olandeză semnificând testare şi incrementare).
Este important de precizat că fiecare dintre cele două operaţii este considerată o acţiune atomică (se
mai spune că este vorba de primitive). Aceasta înseamnă că de exemplu pentru operaţia wait eventuala
succesiune comparare + decrementare este indivizibilă. Analog, în cazul operaţiei signal succesiunile
comparare+deblocare şi comparare+incrementare sunt indivizibile. Este uşor de anticipat că aceasta va
permite rezolvarea simplă a problemei excluderii reciproce.
Mai precizăm că în timp ce două operaţii asupra aceluiaşi semafor se execută prin excludere reciprocă,
pentru două operaţii asupra unor semafoare distincte o suprapunere a executării lor este posibilă.

În cele ce urmează dăm o imagine plastică a modului de funcţionare a unui semafor.


Considerăm o parcare pentru maşini prevăzută cu o intrare şi o ieşire. Activităţile din parcare sunt
dirijate de un paznic, care autorizează intrările (respectiv ieşirile) maşinilor în (respectiv din) parcare şi ţine
minte numărul s de locuri libere. În continuare arătăm că activitatea paznicului este similară cu funcţionarea
unui semafor.
Fiecare şofer, înainte de a încerca să intre sau să iasă din parcaj, anunţă mai întâi paznicul. Acesta, fiind
un om obişnuit, nu poate discuta simultan cu mai multe persoane, deci niciun şofer nu se poate adresa
paznicului în intervalul de timp în care acesta dialoghează cu un alt şofer (asigurându-se astfel excluderea
reciprocă).
Există două modalităţi prin care paznicul discută cu şoferii, corespunzătoare celor două primitive
asupra semafoarelor:
- wait(s) : Un şofer anunţă paznicul că doreşte să intre în parcare. Paznicul verifică dacă există locuri
libere. În caz afirmativ, şoferului i se permite accesul, maşina ocupă unul dintre locurile libere, valoarea lui s se
micşorează cu o unitate şi dialogul se încheie. În caz contrar, paznicul comunică şoferului că nu poate intra
deoarece nu există locuri libere, dar că a notat numărul maşinii, invitându-l să fie gata să intre în parcar atunci
când îi va face semn; dialogul se încheie.
- signal(s) : Un şofer doreşte să iasă din parcaj şi începe dialogul cu paznicul prin a îl informa de această
intenţie. Dacă există şoferi care aşteaptă să intre în parcare, atunci paznicul îi permite să iasă din parcare
şoferului care a iniţiat dialogul, concomitent (pentru operativitate) făcând semn să intre unuia dintre şoferii
aflaţi pe lista de aşteptare a paznicului; dialogul se încheie. În caz contrar, paznicul autorizează părăsirea
parcajului de către şoferul ce a iniţiat dialogul, măreşte cu o unitate valoarea lui s şi încheie dialogul.
Pe lângă cele două primitive prezentate mai sus, singura operaţie permisă (şi necesară) asupra
semafoarelor este atribuirea unei valori iniţiale.
Recapitulând, descriem în pseudocod tipul semafor:
Valori posibile: numere naturale.
Declararea unei variabile de tip semafor:
var s:semaphore;
Operaţii permise:
1) initial(s,s0)
2) wait(s) :
if s>0
then ss-1
else procesul curent este blocat
3) signal(s) :
if există procese blocate
then unul dintre ele este deblocat
else ss+1
Operaţiile wait şi signal asupra aceluiaşi semafor se execută prin excludere reciprocă.

Invarianţi
Pentru un semafor s oarecare, următoarele relaţii sunt invariante:
s>=0 (1)
s=s0+#(signal)-#(wait) (2)
unde s0 este valoarea iniţială a semaforului, #(wait) indică de câte ori s-a trecut complet de semafor (nu se
numără deci procesele temporar blocate la semafor), iar #(signal) este numărul operaţiilor signal executate.
Relaţiile (1) şi (2) sunt invariante în sensul că sunt îndeplinite la orice moment de timp.
Să verificăm de exemplu relaţia (2). Ea este satisfăcută la iniţializarea semaforului. Fie un moment de
timp oarecare la care relaţia este îndeplinită şi să considerăm prima operaţie ulterioară asupra semaforului.
Distingem cazurile:
- se execută o operaţie wait: dacă s=0 atunci cantităţile ce intervin în relaţie rămân neschimbate; dacă s>0
atunci s scade cu o unitate, iar #(wait) creşte cu o unitate;
- se execută o operaţie signal: dacă există procese blocate de semaforul s atunci #(signal) şi #(wait) cresc cu
o unitate, valoarea lui s rămânând astfel neschimbată; în caz contrar, #(signal) şi s cresc cu o unitate.
Excluderea reciprocă
Pentru realizarea excluderii reciproce, vom plasa secţiunea critică între "parantezele" wait şi signal:
wait(s);
SC
signal(s);
SNC

Semaforul s trebuie iniţializat cu 1: dacă ar fi iniţializat cu 0, în momentul în care oricare dintre


procese ar încerca să intre în secţiunea sa critică, el ar fi suspendat la executarea primitivei wait(s),
contrazicându-se astfel condiţia de competiţie constructivă; dacă ar fi iniţializat cu o valoare mai mare decât
1, atunci două procese care doresc amândouă să intre în secţiunea lor critică vor reuşi acest lucru,
contrazicându-se condiţia de excludere reciprocă.
În continuare vom considera numai momentele de timp când toate procesele sunt, într-o distribuţie
oarecare, în secţiunile lor critice, necritice sau sunt blocate (deci nici unul dintre procese nu este în curs de
executare a primitivelor wait şi signal); de exemplu după ce un proces a executat instrucţiunea signal, el este
considerat deja ca fiind în secţiunea necritică şi nu urmând să intre în aceasta.
Pentru a demonstra corectitudinea algoritmului, vom arăta că:
s+#(SC)=1 (3)
este o relaţie invariantă, unde #(SC) este numărul de procese ce se află în secţiunea lor critică.
Ţinând cont de relaţia invariantă (2) şi de faptul că s0=1, este suficient să demonstrăm că:
#(SC)=#(wait)-#(signal).
Această ultimă relaţie este iniţial adevărată şi rămâne adevărată atât la intrarea oricărui proces în
secţiunea critică (se măresc concomitent #(SC) şi #(wait)), cât şi la intrarea oricărui proces în secţiunea sa
necritică (se micşorează #(SC) şi se măreşte #(signal)).
 precum şi cea de
Din relaţia (3) rezultă că este îndeplinită condiţia de excludere reciprocă (#(SC)1),
competiţie constructivă (dacă nici un proces nu este în secţiunea critică, atunci s=1 şi deci unul dintre ele va
intra sigur). Condiţia de conexiune liberă este şi ea evident satisfăcută.
Excluderea reciprocă
Pentru realizarea excluderii reciproce, vom plasa secţiunea critică între "parantezele" wait şi signal:

wait(s);
SC
signal(s);
SNC

Semaforul s trebuie iniţializat cu 1: dacă ar fi iniţializat cu 0, în momentul în care oricare dintre procese
ar încerca să intre în secţiunea sa critică, el ar fi suspendat la executarea primitivei wait(s), contrazicându-se
astfel condiţia de competiţie constructivă; dacă ar fi iniţializat cu o valoare mai mare decât 1, atunci două
procese care doresc amândouă să intre în secţiunea lor critică vor reuşi acest lucru, contrazicându-se condiţia
de excludere reciprocă.
În continuare vom considera numai momentele de timp când toate procesele sunt, într-o distribuţie
oarecare, în secţiunile lor critice, necritice sau sunt blocate (deci nici unul dintre procese nu este în curs de
executare a primitivelor wait şi signal); de exemplu după ce un proces a executat instrucţiunea signal, el este
considerat deja ca fiind în secţiunea necritică şi nu urmând să intre în aceasta.

Pentru a demonstra corectitudinea algoritmului, vom arăta că:


s+#(SC)=1 (3)
este o relaţie invariantă, unde #(SC) este numărul de procese ce se află în secţiunea lor critică.
Ţinând cont de relaţia invariantă (2) şi de faptul că s0=1, este suficient să demonstrăm că:
#(SC)=#(wait)-#(signal).
Această ultimă relaţie este iniţial adevărată şi rămâne adevărată atât la intrarea oricărui proces în
secţiunea critică (se măresc concomitent #(SC) şi #(wait)), cât şi la intrarea oricărui proces în secţiunea sa
necritică (se micşorează #(SC) şi se măreşte #(signal)).
Din relaţia (3) rezultă că este îndeplinită condiţia de excludere reciprocă (#(SC)1), precum şi cea de
competiţie constructivă (dacă nici un proces nu este în secţiunea critică, atunci s=1 şi deci unul dintre ele va
intra sigur). Condiţia de conexiune liberă este şi ea evident satisfăcută.

O implementare simplă a semafoarelor


O primă implementare este oferită de următoarea clasă:
class Semafor {
private int val = 0;

public Semafor(int initial) { val = initial; }

public synchronized void W() {


val--;
if (val < 0) try { wait(); }
catch(InterruptedException e) { }
}

public synchronized void S() {


val++;
if (val <=0 ) notify();
}
}

Observaţii:
- constructorul clasei realizează, odată cu crearea unui obiect, şi iniţializarea valorii val a semaforului;
- metodele W şi S corespund primitivelor wait şi signal descrise mai sus, ce operează asupra
semafoarelor;
- fie sem un semafor, deci un obiect de tipul Semafor. Atunci operaţiile wait(sem) şi signal(sem) din
modelul general propus de Dijkstra vor trebui înlocuite respectiv prin sem.W() şi sem.S().

Propoziţie. Fie s un semafor (obiect de tipul clasei Semafor). Există procese blocate prin metoda W
în mulţimea Ws dacă şi numai dacă val<0; în acest caz, numărul lor este -val.

Afirmaţia este evident adevărată la momentul iniţial (crearea semaforului s).


Când un fir execută metoda W prin intermediul semaforului s:
- dacă val>0, atunci val-- şi metoda se încheie;
- dacă val<=0, atunci val-- şi firul este blocat în Ws; numărul firelor blocate este -val.
Când un fir execută metoda S prin intermediul semaforului s:
- dacă nu există fire blocate la semaforul s, atunci val++ şi metoda se încheie;
- dacă există fire blocate la semaforul s, atunci val++, corespunzător faptului ca un fir din Ws trece în Ms
prin executarea lui notify(); terminarea metodei face ca un fir din Ms să treacă în starea "gata de
executare". Drept urmare, fiecare dintre firele blocate prin W are şansa ca la un moment dat să-şi reia
activitatea şi să termine de executat metoda W.

Exemplul 1. Problema grădinilor ornamentale.

Reluăm problema grădinilor ornamentale. În plus vom crea un bazin de fire de executare cu număr
fix de fire. Vom folosi două semafoare: ER (pentru excludere reciprocă) şi s (pentru simularea bazinului de
fire).

class C {
static int n; int r;
static Semafor s = new Semafor(3);
static Semafor ER = new Semafor(1);

void incr() {
ER.W();
r = n; r++; n = r;
ER.S();
}
}

class Tip extends Thread {


C Ob = new C(); int i;
Tip(int i) { this.i = i; }

public void run() {


C.s.W();
try {
for (int j=1; j<=20; j++) {
Thread.sleep( (int) (50*Math.random()) );
Ob.incr(); System.out.print(" "+i);
}
}
catch(InterruptedException ie) { }
C.s.S();
}
}

class AtribMult1 {
public static void main(String[] s)
throws InterruptedException {
Tip[] T = new Tip[10];
for (int i=0; i<10; i++) T[i] = new Tip(i);
for (int i=0; i<10; i++) T[i].start();
for (int i=9; i>=0; i--) T[i].join();
System.out.println("\nn = " + C.n);
}
}

Problema Producător - Consumator


Reluăm aceată problemă, rezolvată anterior cu monitoare.
Fie lung lungimea benzii. Vom folosi semafoarele libere şi ocupate, a căror valoare corespunde
numărului de celule libere, respectiv ocupate de pe bandă; corespunzător, ele vor fi iniţializate cu lung
(lungimea benzii), respectiv 0.
Să observăm că este verificată relaţia libere+ocupate=lung; aceasta nu permite însă folosirea unui
singur semafor!

Procesul Prod

for ch='a','z'
wait(libere);
wait(ExRec);
pune(ch); write(" P" + ch);
signal(ExRec);
signal(ocupate)
end

Procesul Cons

repeat
wait(ocupate);
wait(ExRec);
ia(ch); write(" P" + ch);
signal(ExRec);
signal(libere)
until ch='z'

La o masă rotundă stau nf filozofi chinezi, iar între fiecare doi filozofi vecini se află un beţişor.
Fiecare filozof execută în mod repetat secvenţa de acţiuni (gândeşte, mănâncă), pentru intervale
variabile de timp:
 ia beţişorul din stânga;
 ia beţişorul din dreapta;
 foloseşte beţişoare pentru a mânca din castronul cu orez din centrul mesei;
 pune pe masă beţişorul din stânga;
 pune pe masă beţişorul din dreapta.
Răspunsul la întrebarea "Ce se poate întâmpla rău" are două aspecte:
 doi filozofi vecini pot lua amândoi beţişorul dintre ei, deci trebuie asigurată excluderea reciprocă asupra
beţişoarelor;
 se poate ajunge la blocare totală (deadlock) în situaţia în care toţi filozofii au ridicat beţişorul din stânga
lor; trebuie deci evitată această situaţie.
Fiecărui filozof i îi asociem un proces Fi.
Pentru rezolvarea excluderii reciproce vom asocia fiecărui beţişor i un semafor binar bi, iniţializat cu 1;
încercarea de ridicare a unui beţişor corespunde interogării semaforului asociat lui.
Pentru evitarea blocării totale introducem semaforul AccesLiber, care va memora în permanenţă
numărul de filozofi cărora le dăm voie să încerce să mănânce. Iniţializarea sa se poate face cu orice valoare
întreagă din intervalul [1,nf-1], dar (pentru maximum de libertate) vom alege valoarea nf-1.
Filozoful Fi
repeat
filozoful i gândeşte
wait(AccesLiber);
wait(b[i]);
wait(b[(i mod nf)+1]);
write(" M" + i);
filozoful i mănâncă
write(" G" + i);
signal(b[i]);
signal(b[(i mod nf)+1]);
signal(AccesLiber)
until false
Mesajul de început al activităţii de a mânca trebuie inserat imediat după ce se trece de semafoarele care
controlează posibilitatea de a fi ridicate beţişoarele din stânga şi dreapta filozofului.
Mai puţin evident este că mesajul de început al activităţii de a gândi trebuie inserat imediat înainte (şi
nu după) semnalările repunerii beţişoarelor pe masă. Dacă mutăm acest mesaj după operaţiile signal ce
corespund repunerii beţişoarelor pe masă, lucrurile se vor desfăşura corect din punctual de vedere al
cerinţelor problemei, dar incorect din punctul de vedere al semnalării acţiunilor; într-adevăr, pe ecran poate să
apară secvenţa:
G4 G3 G1 G5 G2 M1 M2 G1 ...
deoarece mesajul "G1 " apare după ce filozoful 1 repune beţişoarele pe masă, între timp filozoful 2 începând să
mănânce.

Exemplul 2. Implementarea cu semafoare a problemei filozofilor chinezi.

import java.util.*;

class Fil extends Thread {


static int n;
static Semafor[] b; static Semafor AccesLiber;
static void delay(int i) {
try { Thread.sleep( (int) (i*Math.random()) ); }
catch(InterruptedException e) { }
}

int id,k;
Fil(int i) { id = i; }
public void run() {
for(k=0; k<10; k++) {
AccesLiber.W();
b[id].W();
b[ (id+1) % n ].W();
System.out.print("M" + id + " ");
delay(100);
System.out.print("G" + id + " ");
b[id].S();
b[ (id+1) % n ].S();
delay(100);
AccesLiber.S();
}
}
}

class FilSem {
public static void main(String[] qqq) {
int i;
Scanner sc = new Scanner(System.in);
System.out.print("Nr. filozofi = ");
Fil.n = sc.nextInt();
Fil.b = new Semafor[Fil.n];
for(i=0; i<Fil.n; i++) Fil.b[i] = new Semafor(1);
Fil.AccesLiber = new Semafor(Fil.n-1);
Fil[] filozofi = new Fil[Fil.n];
for(i=0; i<Fil.n; i++) filozofi[i] = new Fil(i);
for(i=0; i<Fil.n; i++) filozofi[i].start();
}
Clasa Semaphore

public class Semaphore extends Object implements Serializable

Această clasă permire lucrul cu semafoare. Valoarea unui semafor reprezintă numărul de permisiuni
de a intra în secţiunea critică (de a accesa resurse comune). Încercarea de a accesa resursele comune se face
prin invocarea metodei acquire(), iar renunţarea la această operaţie se face prin invocarea metodei
release(); cele două metode corespund lui wait şi signal din definiţia general a semafoarelor.
Un semafor iniţializat cu 1, numit şi semafor binar, permite realizarea excluderii reciproce.
Unul dintre constructorii clasei permite şi specificarea unui parametru de echitate (fairness), care -
dacă este setat pe true - asigură deblocarea conform unei discipline de coadă.
Nu este impus ca orice release() să fie precedat o invocare acquire(). Utilizarea corectă a
semfoarelor ţine de aplicaţia concretă.
Spre deosebire de alte mecanisme de programare concurentă, semafoarele (care nu au noţiunea de
posesie a unui lacăt) au proprietatea că "lacătul" poate fi deschis de un alt fir (nu există restricţia ca
metodele acquire şi release asupra aceluiaşi semafor să apară în acelaşi fir).
Cei doi constructori ai clasei sunt:

public Semaphore(int n)
iniţializează la n valoarea semaforului şi nu asigură fairness la deblocarea firelor prin release(). Dacă
n<0, sunt necesare invocări release() înainte ca o invocare să realizeze accesul la secţiunea critică.
public Semaphore(int n, boolean fair)
diferă de constructorul anterior prin aceea că dacă al doilea argument este true, la deblocare este
asigurată disciplina de coadă. Această disciplină este însă relativă la puncte de executare specifice din
metodele acquire şi release, ceea ce poate conduce la ordini întrucâtva diferite de cele aşteptate;
important este însă că în mod sigur este evitată situaţia de "starvation" (amânare infinită), adică
eşuarea continuă de a accesa o resursă.

Prezentăm în continuare principalele metode ale clasei.


public void acquire() throws InterruptedException
Dacă valoarea semaforului este pozitivă, această valoare este decrementată cu o unitate. În caz contrar,
firul este blocat până când un alt fir execută release() şi firul curent este cel ales să treacă în starea gata
de executare. Este echivalentul lui wait.
Excepţia este lansată dacă firul este în starea "întrerupt" la intrarea în metodă sau dacă este întrerupt în
timp ce aşteaptă permisiunea de a intra în secţiunea critică; în aceste cazuri firul iese din starea
"întrerupt".
public boolean tryAcquire()
Întoarce rezultatul încercării de a obţine o permisiune. Dacă este posibil să obţină o permisiune, o
obţine chiar dacă semaforul urmează politica de fairness (deci o obţine înaintea altor fire care sunt
blocate) şi decrementează valoarea semaforului; în acest caz valoarea întoarsă este true. Dacă nu se
poate obţine o permisiune, metoda întoarce valoarea false şi firul care a invocat-o îşi continuă
activitatea.
public void release()
Eliberează o permisiune, incrementând numărul permisiunilor cu o unitate. Dacă există fire ce aşteaptă
o permisiune, este ales unul dintre ele; el capătă permisiunea (deci valoarea semaforului scade cu o
unitate) şi trece în starea gata de executare. Este echivalentul lui signal.
public int availablePermits()
Întoarce valoarea curentă (numărul curent de permisiuni) a semaforului; este folosită de obicei pentru
depanare.

Clasa Semaphore pune la dispoziţie şi metode pentru cereri/eliberări de permisiuni multiple, dintre
care unele sunt prezentate în continuare. Este recomandat să fie folosite în modul fair, pentru a nu conduce
la amânare infinită.
Excepţia IllegalArgumentException este lansată dacă p<0.

public void acquire(int p) throws InterruptedException,


IllegalArgumentException
Dacă valoarea semaforului mai mare sau egală cu p, această valoare este decrementată cu p. În caz
contrar, firul iese din schema de programare a firelor până când fie un alt fir execută release(), firul
curent este cel ales să-şi reia activitatea şi valoarea semaforului este cel puţin egală cu p, fie un alt fir
întrerupe pe cel curent.
Excepţia InterruptedException este lansată dacă firul este în starea "întrerupt" la intrarea în metodă sau
dacă este întrerupt în timp ce aşteaptă permisiunea de a intra în secţiunea critică; în aceste cazuri firul
iese din starea "întrerupt"; toate permisiunile acordate acestui fir sunt transferate altor fire care
aşteaptă să obţină permisiuni, ca şi când au devenit disponibile permisiuni prin executarea unui
release().

public boolean tryAcquire(int p) throws IllegalArgumentException


Dacă valoarea semaforului este cel puţin p, sunt obţinute p permisiuni (vezi şi observaţia de la forma
fără argumente a metodei), valoarea semaforului este decrementată cu p şi rezultatul întors este true.
În caz contrar, valoarea semaforului rămâne neschimbată şi rezultatul întors este false.

public void release(int p) throws IllegalArgumentException


Incrementează cu p valoarea semaforului. Cât timp valoarea semaforului este strict pozitivă şi există fire
blocate la semafor, se execută următoarele acţiuni:
1. este ales unul dintre firele blocate; fie p' numărul de permisiuni de care are nevoie;
2. i se atribuie min(p,p') permisiuni, cu actualizările de rigoare; dacă i s-au atribuit p' permisiuni, firul
reintră în schema de gestiune a firelor.

Observaţii:
- metodele release cu şi fără argumente lucrează diferit, deci (cred că) trebuie lucrat ori cu permisiuni
unice, ori cu permisiuni multiple !
- metodele tryAcquire (cu cerere de permisiune unică sau multiplă) au câte o variantă pentru timp real.

Exemplul 3. Fie s,t două tablouri cu ns şi respectiv nt elemente. Se cere să se elaboreze un program
concurent care să înscrie în s cele mai mici ns, iar în t cele mai mari nt elemente din cele două tablouri. Singura
operaţie posibilă ce implică un element din s şi un element din t este interschimbarea lor.

Se urmează ideea de la problema Producător - Consumator cu o bandă de lungime 1.


La fiecare pas, firul FirS calculează maximul valorilor din s şi îl transmite (îl pune la dispoziţia) firului
FirT. Acesta calculează minimul valorilor din t şi, dacă valoarea primită este mai mare decât minimul, se face
interschimbare. Facem observaţia că, întrucât lungimea benzii este 1, acţiunile firelor FirS şi FirT se succed în
mod repetat în această ordine.
Entităţile comune apar în clasa C. Semafoarele s1 şi s2 corespund semafoarelor libere şi ocupate din
problema Producător - Consumator, fiind iniţializate şi folosite ca atare.
import java.util.*;
import java.util.concurrent.*;

class MinMax {
public static void main(String[] www) throws Exception {
S FirS = new S(); T FirT = new T();
FirS.start(); FirT.start();
FirS.join(); FirT.join();
C.scrie();
}
}

class C { // contine entitatile comune


static Scanner sc = new Scanner(System.in);
static int[] s,t; static int ns,nt;
static int ks,kt,maxs,mint;
static boolean continua=true;
static Semaphore s1 = new Semaphore(1),
s2 = new Semaphore(0);

static void scrie() {


for(int i=0; i<ns; i++) System.out.print(s[i] + "\t");
System.out.println();
for(int i=0; i<nt; i++) System.out.print(t[i] + "\t");
}
}

class S extends Thread {


S() {
System.out.print("ns = ");
C.ns = C.sc.nextInt();
C.s = new int[C.ns];
System.out.print("Dati cele ns=" + C.ns +" elemente: ");
for(int i=0; i<C.ns; i++) C.s[i] = C.sc.nextInt();
}

public void run() {


try {
while(C.continua) {
C.s1.acquire();
C.ks = 0; C.maxs=C.s[0];
for(int i=1; i<C.ns; i++)
if(C.s[i]>C.maxs) { C.maxs = C.s[i]; C.ks = i; }
C.s2.release();
}
}
catch(Exception e) { }
}
}
class T extends Thread {
T() {
System.out.print("nt = "); C.nt = C.sc.nextInt();
C.t = new int[C.nt];
System.out.print("Dati cele nt=" + C.nt +" elemente: ");
for(int i=0; i<C.nt; i++) C.t[i] = C.sc.nextInt();
}

public void run() {


try {
while(C.continua) {
C.s2.acquire();
C.kt = 0; C.mint=C.t[0];
for(int i=1; i<C.nt; i++)
if(C.t[i]<C.mint) { C.mint = C.t[i]; C.kt = i; }
if(C.maxs<=C.mint) C.continua = false;
else {
int x = C.s[C.ks]; C.s[C.ks] = C.t[C.kt];
C.t[C.kt] = x;
}
C.s1.release();
}
}
catch(Exception e) { }
}
}

Observaţie. Deşi este respectată condiţia ca singura operaţie ce implică un element din s şi un element
din t să fie interschimbarea lor, soluţia de mai sus are dezavantajul că ambele fire au acces la ambele tablouri.

Problema Cititori - Scriitori


Se consideră o carte la care au acces mai mulţi cititori şi mai mulţi scriitori. Este permis ca mai mulţi cititori să
aibă acces simultan la conţinutul cărţii, dar dacă un scriitor operează asupra cărţii (completează, şterge,
modifică etc.) atunci nici un alt scriitor şi nici un alt cititor nu au acces la carte. Mai precis:
1) un cititor poate începe operaţia de citire dacă şi numai dacă nici un scriitor nu este în curs de a scrie în
carte;
2) un scriitor poate începe operaţia de scriere dacă şi numai dacă nici un cititor şi nici un alt scriitor nu au
acces la carte.
Cu alte cuvinte, dacă cuplul (nrcit,nrscr) ţine evidenţa nunărului de cititori activi şi numărul de scriitori
activi, valorile permise ale cuplului sunt numai:
(0,0), (0,1) şi (n,0) cu n>0.

Încercările de deschidere a cărţii au şanse de reuşită doar dacă nici un scriitor nu este activ. În acest
caz ele se împart în două categorii:
a) cele de citire când există cel puţin un cititor activ;
b) cele de citire când nu există cititori activi + cele de scriere.
Încercările din prima categorie trebuie satisfăcute necondiţionat.
Vom considera încercările de deschidere a cărţii din a doua categorie ca fiind egal rivale; drept urmare
vom controla executarea lor printr-un semafor sem ce trebuie iniţializat cu 1, deoarece orice primă încercare
de acest tip trebuie validată. Din motive de simetrie, sem va fi actualizat (prin operaţia signal) în exact aceleaşi
situaţii. Semaforul sem va avea valoarea 0 dacă şi numai dacă la carte are acces cel puţin o persoană.

Semafoarele ExRec şi s sunt folosite pentru excludere reciprocă, deci sunt iniţializate cu 1.
Procesele Cititori :
repeat
deschide(i,citire); preia informaţia dorită
inchide(i,citire); prelucrează informaţia citită
until false

Procesele Scriitori :
repeat
redactează un text nou; deschide(i,scriere);
introdu textul in carte; inchide(i,scriere);
until false

procedure deschide(i,caz)
if caz=citire
then wait(ExRec);
if nrcit=0 then wait(sem);
wait(s); write(" C" + i + "("); signal(s);
nrcit++;
signal(ExRec);
else wait(sem);
wait(s); write((" S" + i + "("); signal(s);
end

procedure inchide(i,caz)
if caz=citire
then wait(ExRec);
wait(s); write(" C" + i + ")"); signal(s);
nrcit--;
if nrcit=0 then signal(sem);
signal(ExRec);
else wait(s); write(" S" + i + ")"); signal(s);
signal(sem)
end

Acţiunile de deschidere şi închidere pentru citire trebuie executate sub excludere reciprocă. Într-
adevăr, la apelul inchide(i,citire) cu nrcit=1, după decrementarea lui nrcit nu este sigur că nrcit=0, deoarece
este posibil ca între timp valoarea "resursei comune" nrcit să se fi modificat printr-un apel deschide(j,citire).
De aceea am folosit semaforul ExRec, iniţializat cu 1.

Observaţie. Dacă am include "pentru siguranţă" şi operaţia de închidere pentru scriere între
wait(ExRec) şi signal(ExRec), se poate ajunge la blocare globală în următoarea situaţie:
 scriitorul 1 deschide cartea; sem şi ExRec au acum valorile 0, respectiv 1;
 cititorul 1 trece de semaforul ExRec şi se blochează la sem; acum ExRec=0;
 în continuare, orice alt proces (în particular scriitorul 1) se va bloca la ExRec.

Notăm prin nrscr numărul de scriitori activi. În cele ce urmează vom înţelege prin stare a programului
tripletul (nrscr,nrcit;sem). Starea iniţială este (0,0;1).
Vom arăta că singurele stări posibile sunt următoarele:
(0,0;1), (1,0;0) şi (0,n;0) cu n>0.

Dacă suntem în starea (0,0;1):


- poate avea loc o deschidere pentru citire sau scriere, ajungându-se în (0,1;0) sau (1,0;0);
- nu poate avea loc o închidere, deoarece forma proceselor presupune că o închidere este precedată de o
deschidere, în contradicţie cu nrscr=nrcit=0.

Dacă suntem în starea (1,0;0), atunci:


- poate avea loc o închidere pentru scriere şi se trece în starea (0,0;1) (dacă nu există procese blocate la
semaforul sem) sau în una din stările (1,0;0), (0,1;0) (dacă există procese blocate la semaforul sem);
- nu poate avea loc o deschidere pentru scriere deoarece sem=0;
- nu poate avea loc o deschidere pentru citire deoarece nrcit=0 şi sem=0;
- nu poate avea loc o închidere pentru citire deoarece nrcit=0.

Dacă suntem în starea (0,n;0) cu n>0:


- poate avea loc o deschidere pentru citire, trecându-se în starea (0,n+1;0);
- nu poate avea loc o deschidere pentru scriere, deoarece sem=0;
- nu poate avea loc o închidere pentru scriere, deoarece nrscr=0;
- dacă n>1, printr-o închidere pentru citire se trece în starea (0,n-1;0);
- dacă nrcit=1, atunci printr-o închidere pentru citire se trece în starea (0,0;1) (dacă nu există procese
blocate la semaforul sem) sau în una din stările (1,0;0), (0,1;0) (dacă există procese blocate la semaforul sem).
Din analiza de mai sus rezultă validitatea procedurilor deschide şi inchide, precum şi faptul că sem este
un semafor binar.

Exemplul 4. Prezentăm programul Java corespunzător problemei Cititori - Scriitori:

import java.util.concurrent.*;

class CitScr {
public static void main(String[] sss) {
Cit[] cititori = new Cit[5];
Scr[] scriitori = new Scr[3];
for(int i=0; i<5; i++) cititori[i] = new Cit(i);
for(int i=0; i<3; i++) scriitori[i] = new Scr(i);
for(int i=0; i<5; i++) cititori[i].start();
for(int i=0; i<3; i++) scriitori[i].start();
}
}

class C {
static int nrcit;
static Semaphore ExRec = new Semaphore(1,true),
sem = new Semaphore(1,true);

static void delay(int i) throws InterruptedException {


Thread.sleep( (int) (i * Math.random()) );
}

static void deschide(int i, String acces)


throws InterruptedException {
if(acces.equals("citire")) {
ExRec.acquire();
if(nrcit==0) sem.acquire();
System.out.print("\tC" + i + "("); nrcit++;
ExRec.release();
}
else {
sem.acquire();
System.out.print("\tS" + i + "(");
}
}
static void inchide(int i, String acces)
throws InterruptedException {
if(acces.equals("citire")) {
ExRec.acquire();
System.out.print("\tC" + i + ")");
nrcit--; if(nrcit==0) sem.release();
ExRec.release();
}
else {
System.out.print("\tS" + i + ")");
sem.release();
}
}
}

class Cit extends Thread {


int i;
Cit(int i) { this.i = i; }
public void run() {
try {
for(int k=0; k<5; k++) {
C.delay(50); C.deschide(i,"citire");
C.delay(100); C.inchide(i,"citire");
}
}
catch(Exception e) { }
}
}

class Scr extends Thread {


int i;
Scr(int i) { this.i = i; }
public void run() {
try {
for(int k=0; k<3; k++) {
C.delay(50); C.deschide(i,"scriere");
C.delay(100); C.inchide(i,"scriere");
}
}
catch(Exception e) { }
}
}

Observaţie. Metodele tryAcquire (cu cerere de permisiune unică sau multiplă) au câte o variantă
pentru timp real.

Bariere

Un semafor având totdeauna valoarea zero, numit şi barieră: mai multe fire se aşteaptă unul pe altul
să îndeplinească o primă acţiune, înainte de a trece mai departe.
Prezentăm în continuare o facilitate mai complexă oferită de ava pentru lucrul cu bariere.

Clasa CyclicBarrier apare în pachetul java.util.concurrent. Sunt disponibili doi constructori:

CyclicBarrier(int n)

CyclicBarrier(int n, Runnable fir)


unde n reprezintă numărul de fire care sunt aşteptate să ajungă la barieră. Pentru a doua formă, acţiunea
precizată pentru fir va fi executată după ce toate cele n fire au ajuns la barieră şi înainte ca ele să-şi reia
executarea.
Bariera se numeşte ciclică deoarece poate fi refolosită după ce toate firele au ajuns la barieră, punct
în care îşi reiau executarea.
Dintre metodele clasei menţionăm doar următoarea:

public int await() throws InterruptedException, BrokenBarrierException


firul curent este ataşat barierei care execută această metodă şi trece în aşteptare până când toate cele
n fire au ajuns la barieră, reluându-şi apoi executarea. Rezultatul întors este k = al câtelea a ajuns firul la
barieră, cu precizarea că 0 corespunde ultimului fir ajuns la barieră.

Exemplul 5. Sunt lansate 5 fire, cu identităţile 0,1,...,4, care execută următoarele acţiuni:
 într-o primă etapă îşi tipăresc de 10 ori identitatea; când toate au ajuns la barieră, se execută firul fir,
care tipăreşte "****";
 cele 5 fire îşi reiau activitatea şi pentru fiecare este afişat al câtelea a ajuns la barieră;
 în a doua etapă se reiau activităţile de mai sus, cu deosebirea că ele îşi tipăresc de 10 ori identitatea
incrementată cu 10.

Este creată o barieră cb de tipul CyclicBarrier, ataşându-i-se firul fir. Bariera este folosită pentru
realizarea primei etape, iar apoi refolosită pentru realizarea celei de a doua etape; în acest scop este necesar
ca firele să invoce încă o dată metoda await prin intermediul barierei.

import java.util.*; import java.util.concurrent.*;

class Tip extends Thread {


int i;
static Runnable fir =
new Runnable() {
public void run() {
try { Thread.sleep(500); }
catch(InterruptedException ie) { }
System.out.println("****");
}
};

static CyclicBarrier cb = new CyclicBarrier(5,fir);


Tip(int ii) { i = ii; }

public void run() {


Random r = new Random();
try {
for(int j=0; j<10; j++) {
sleep(10+r.nextInt(15)); System.out.print(i + "\t");
}
System.out.println("!!!!" + i + ":" + cb.await() + "!!!!");

for(int j=0; j<10; j++) {


sleep(10+r.nextInt(15)); System.out.print((i+10) + "\t");
}
System.out.println("!!!!" + i + ":" + cb.await() + "!!!!");
}
catch(Exception e) { }
}
}

class Bariera {
public static void main(String[] sss) throws Exception {
for(int i=0; i<5; i++) new Tip(i).start();
}
}

Desfăşurarea lucrării:
1. Se copiază exemplele prezentate şi se vor analiza rezultatele prin repetarea execuției
2. Se vor extinde instanțierile existente şi se va face o analiză a rezultatului
LABORATORUL NR.5
Implementarea şi testare
algoritm de sortare paralelă

Scop:
1.

Consideraţii teoretice:
Putem clasifica algoritmii de sortare după o serie de criterii. Cei mai cunoscuți
algoritmi sunt cei prin comparație, iar cel mai comun criteriu de clasificare a acestora este
după metoda generală de lucru. Astfel avem algoritmi de sortare prin:
Inserție:
• Insertion sort – un algoritm de sortare eficient pe liste de intrare mici sau
aproape sortate
• Shellsort
• Binary tree sort
• Cyclesort – un algoritm cu numărul de scrieri în memorie redus
Selecție:
• Selectionsort – eficient pe liste de intrare mici
• Heapsort – un algoritm de sortare cu timp de execuție constant
• Smoothsort – inspirat de Heapsort, dezvoltat de către Edsger Dijkstra
Algoritmii de sortare prin inserție şi selecție sunt în general algoritmi care nu pot fi uşor paralelizați, dar pot
fi folosiți împreună cu alți algoritmi pentru a forma algoritmi de sortare hibrizi. Mai avem algoritmi de
sortare prin:
Partiționare:
• Quicksort – unul dintre cei mai cunoscuți algoritmi de sortare
Introsort – un hibrid între Quicksort şi Heapsort
Interclasare (merging):
• Mergesort
• Timsort – este un hibrid între Mergesort şi Insertion sort
Distribuire:
• Bucketsort
Algoritmii prin partiționare, interclasare şi distribuire sunt algoritmii care pot fi
paralelizați cel mai uşor datorită naturii acestora.
Printre cei mai lenți algoritmi se numără cei prin inter-schimbare:
• Bubblesort
• Odd-even sort - o variație uşor îmbunătățită a Bubble-sort
Toți algoritmii prezentați anterior fac parte din categoria algoritmilor de sortare prin comparație. Exemple de
algoritmi de sortare care nu folosesc comparația sunt:
• Radix sort
• Bead sort
Radix sort spre exemplu datează din 1887, fiind folosit de către Herman Hollerith pe maşini tabelare.
Modul de lucru al acestuia nu presupune comparația numerelor din lista de intrare, ci comparația cifrelor
numerelor, în funcție de poziția pe care o ocupă acestea.
O altă metodă de clasificare a algoritmilor este în funcție de stabilitatea acestora. Un algoritm de
sortare stabil va păstra ordinea elementelor care au chei egale. Astfel dacă avem de sortat un şir de
dicționare cum ar fi:
{2: 'amet'} {1: 'dolor'} {1: 'sit'} {0: 'lorem'} {0: 'ipsum'}
Un algoritm de sortare stabil va produce:
{0: 'lorem'} {0: 'ipsum'} {1: 'dolor'} {1: 'sit'} {2: 'amet'}
Păstrând astfel ordinea inițială a elementelor care au chei egale. Dintre algoritmii menționați urmează să
prezentăm pe larg câțiva dintre ei, şi anume: Quicksort, Mergesort, Heapsort şi Selection sort.
1.2. Quicksort
Quicksort este un algoritm de sortare conceput de către C.A.R. Hoare în 1962. În
practică este unul dintre cei mai rapizi algoritmi de sortare. Quicksort face în medie
Ο(nlog (n)) comparații atunci când mărimea listei de intrare este de n elemente. De regulă
este mai rapid decât alți algoritmi Ο(nlog (n)) . În cel mai rău caz, Quicksort face Ο(n2)
comparații, deşi acest caz este de obicei rar. Nu este un algoritm de sortare stabil.
Quicksort se bazează pe principiul divide et impera. Algoritmul împarte lista de
intrare în două sub-liste mai mici, pe care le sortează în mod recursiv, reaplicând acelaşi
algoritm. Paşii efectivi sunt:
• Alege un element, numit pivot, din listă.
• Re-aranjează lista în aşa fel încât elementele mai mici decât pivotul vor fi
în stânga acestuia, iar elementele mai mari vor fi în dreapta. Această
operație se numeşte partiționare.
• Aplică recursiv algoritmul pe sub-lista cu elemente mai mici, şi pe sublista
cu elemente mai mari.
• Atunci când mărimea listei de intrare este 0 sau 1, nu este nevoie să mai
re-aplicăm algoritmul
Are o complexitate de Ο(nlog (n)) . La o analiză a acestuia, îl putem descompune la fiecare pas în operațiile
executate şi stabili astfel complexitatea sa.
Operația de partiționare, care iterează o dată peste elementele listei de intrare are o complexitate de Ο(n) .
În cazul favorabil, dacă pivotul selectat împarte lista în două subliste de dimensiuni aproximativ egale,
înseamnă că fiecare apel recursiv va procesa jumătate din datele de intrare inițiale. În consecință vom face
Ο(logn) apeluri până când vom ajunge la o listă de dimensiunea 1, pe care, evident, nu mai este nevoie să o
sortăm. Înseamnă că înălțimea arborelui format de către apelurile recursive va fi Ο(logn) . Dar apelurile
situate la acelaşi nivel al arborelui nu vor procesa aceeaşi parte a listei inițiale, deci fiecare nivel va avea Ο(n)
comparații. Astfel algoritmul final foloseşte Ο(nlog (n)) comparații.
O altă metodă de analiză este să folosim o relație de recurență, unde T (n) reprezintă timpul necesar pentru
a sorta o listă de mărimea n. Pentru că un singur apel al Quicksort înseamnă Ο(n) plus încă două apeluri pe
liste de dimensiunea n /2 , în cazul cel mai favorabil, relația va fi: T (n)=Ο(n)+2T (n/2) , rezultând în T
(n)=Ο(nlog (n)) , conform Master Theorem [6].
În cazul nefavorabil, când cele două sub-liste au dimensiunea 1 şi n-1 arborele construit de către apelurile
recursive devine liniar. Astfel relația de recurență va fi:
T (n)=Ο(n)+T (0)+T (n−1) care va rezulta în T (n)=Ο(n2) [7] [8].
De menționat că spațiul în memorie folosit de către Quicksort este Ο(logn) ,chiar şi în cazul nefavorabil.
1.3. Mergesort
Mergesort este un algoritm de sortare descoperit de către John von Neumann în 1945. La fel ca şi
Quicksort, este un algoritm care se bazează pe principiul divide et impera. Acesta face atât în medie cât şi în
cel mai rău caz Ο(nlog (n)) comparații. În cazul Mergesort, o funcție importantă este cea de interclasare
(merge). De reținut că acest algoritm este unul stabil.
Paşii pentru realizarea Mergesort sunt:
• Împarte lista inițială în două sub-liste de mărimi egale. De notat este că nu folosim un pivot.
• Aplică recursiv algoritmul pe fiecare dintre sub-liste.
• Atunci când mărimea listei de intrare este 0 sau 1, lista este considerată
sortată.
• Interclasează sub-listele sortate într-o singură listă . Algoritmul se bazează pe două principii simple pentru
a câştiga performanță:
• O listă mai mică va avea nevoie de mai puțini paşi pentru a fi sortată decât o lista de dimensiuni mari.
• Sunt necesari mai puțini paşi pentru a construi o listă sortată din două subliste sortate, decât două sub-
liste ne-sortate.
Atât în cazul favorabil cât şi în cel nefavorabil, Mergesort are un timp de execuție de Ο(nlog (n)) . Relația de
recurență a algoritmului va fi:

Deoarece avem nevoie de T (n/2) timp pentru a sorta fiecare sub-listă şi de n


timp pentru operația de interclasare (merge). Mai jos avem o reprezentare grafică a arborelui
de recurență pentru Mergesort. Menționăm că spațiul în memorie folosit de către Mergesort
este Ο(n) .
Paralelizarea algoritmilor
Dacă în mod secvențial nu trebuie să ținem cont în mod deosebit de nivelul de
recurență al algoritmului, atunci când vrem sa-l paralelizăm, acest nivel de
recurență va juca
un rol esențial. Luând Quicksort ca exemplu, va trebui să-l distribuim în mai
multe procese,
care vor rula în mod paralel. Această distribuire o vom face atunci când lista
de intrare este
împărțită în două sub-liste de către elementul pivot. Astfel vom putea lansa
câte un proces
independent pentru fiecare dintre cele două sub-liste.
Mergând în jos pe arborele de recursivitate, cele două procese vor lansa la
rândul
lor câte alte două sub-procese, obținând astfel un arbore de procese care se
suprapune
arborelui de recursivitate al algoritmului. Cu toate acestea nu vom continua să
lansăm procese
până la ultimul nivel al arborelui. Ne vom limita la a lansa procese doar
pentru primele câteva
niveluri de recurență.
Vom nota aceste niveluri astfel:
• N0 – este nivelul rădăcină și rezultă din apelul inițial al funcției, pe
lista
inițială de intrare.
• N1 – este rezultatul a 21 apeluri ale funcției, fiecare pe câte o sub-listă.
• N2 – este rezultatul a 22 apeluri, fiecare pe câte o sub-listă a nivelului
anterior.
18

În implementarea de Quicksort şi Mergesort dezvoltată, avem posibilitatea să lansăm în execuție 2i


procese separate la fiecare nivel, acolo unde i este indicele nivelului. Astfel, vom putea lansa un Quicksort
distribuit pe oricâte procese permite arborele de recurență, într-un caz extrem, putem chiar să lansăm câte
un proces separat pentru fiecare nivel de recursivitate. Cu toate acestea, pentru experimentele realizate ne-
am limitat doar la primele patru niveluri (N0 – N3), respectiv 1, 2, 4 sau 8 procese.
După ce nivelul de recursivitate va depăşi nivelul până la care facem distribuirea pe procese,
algoritmul va rula în continuare în mod secvențial, apelurile recursive ale funcției având loc în cadrul
procesului părinte. Fiecare dintre aceste apeluri vor sorta lista şi vor returna rezultatele apelului părinte. În
cazul proceselor lansate, acestea vor returna părintelui o structură de date de tip coadă care va conține lista
sortată. Această coadă nu va fi un obiect global, ci unul local, accesibil doar de procesul părinte şi cei doi
copii ai săi. Procesul părinte după ce lansează cele doua procese copil urmăreşte această coadă pentru
rezultate. Imediat ce obține cele două rezultate de la procesele copil le va concatena cu pivotul (dacă este
cazul) şi va returna rezultatul procesului său părinte. În cazul în care acesta este deja procesul rădăcină, va
returna lista sortată.
Deoarece coada este un obiect local, aceasta va fi creată de fiecare proces părinte care urmează să
lanseze sub-procese. Acest Inter-Process-Communication (IPC) se va realiza prin intermediul cozii doar între
procese părinte-copil separate. Între apeluri de funcție obişnuite, comunicarea se va realiza prin returnare
obişnuită a valorii.

package com.java2novice.sorting;
public class MyQuickSort {

private int array[];


private int length;

public void sort(int[] inputArr) {

if (inputArr == null || inputArr.length == 0) {


return;
}
this.array = inputArr;
length = inputArr.length;
quickSort(0, length - 1);
}

private void quickSort(int lowerIndex, int higherIndex) {

int i = lowerIndex;
int j = higherIndex;
// calculate pivot number, I am taking pivot as middle index number
int pivot = array[lowerIndex+(higherIndex-lowerIndex)/2];
// Divide into two arrays
while (i <= j) {
/**
* In each iteration, we will identify a number from left side which
* is greater then the pivot value, and also we will identify a
number
* from right side which is less then the pivot value. Once the
search
* is done, then we exchange both numbers.
*/
while (array[i] < pivot) {
i++;
}
while (array[j] > pivot) {
j--;
}
if (i <= j) {
exchangeNumbers(i, j);
//move index to next position on both sides
i++;
j--;
}
}
// call quickSort() method recursively
if (lowerIndex < j)
quickSort(lowerIndex, j);
if (i < higherIndex)
quickSort(i, higherIndex);
}

private void exchangeNumbers(int i, int j) {


int temp = array[i];
array[i] = array[j];
array[j] = temp;
}

public static void main(String a[]){

MyQuickSort sorter = new MyQuickSort();


int[] input = {24,2,45,20,56,75,2,56,99,53,12};
sorter.sort(input);
for(int i:input){
System.out.print(i);
System.out.print(" ");
}
}
}

Desfăşurarea lucrării:
1. Se copiază exemplele prezentate şi se vor analiza rezultatele prin repetarea execuției
2. Se vor extinde instanțierile existente și se va face o analiză a rezultatului
LABORATORUL NR.6
Implementarea unor algoritmi
din metodele numerice

Scop:
1. Implementarea unor algoritmi numerici prin utilizarea calcului paralel
Consideraţii teoretice:
Metoda de optimizare Secţiunea de aur
Metoda este utilizată pentru optimizarea sistemelor monovariabile (descrise prin funcţii monovariabile)
neliniare cu sau fără restricţii.
Algoritmul:
Pasul 1 Se alege un punct arbitrar x0 şi se calculează funcţia criteriu în acest punct.
Pasul 2 Se evaluează funcţia criteriu în x 0  x . Dacă s-a obţinut o îmbunătaţire se dublează x până se
obţine un eşec.
Pasul3 Dacă nu s-a obţinut o îmbunătăţire în prima fază a pasului 2, se schimbă sensul de deplasare. Se
calculează F în x 0  x . În caz de îmbunătăţire se dublează x până se obţine un eşec.
Pasul 4 Dacă în caz că nu s-a reuşit să se obţină o îmbunătăţire în prima fază nici la pasul 2 nici la pasul 3, se

1
face x  x şi se reia cu pasul 2.
2
Pasul 5 Se reţin ultimele două puncte (eşecul şi succesul anterior eşecului) şi se ordonează (în ordine

crescătoare) a1  b1 . Intervalul L1  (a1 , b1 ) reprezintă restricţia problemei: minimizează F(x) cu

a1  x  b1 .

Pasul 6 Se determină punctele x1 si x2 din L1 prin relaţiile:


b1  a1
x1  a1  l1  a1   a1  0,382(b1  a1 )
2
b1  a1
x2  b1  l1  b1   a1  0,618(b1  a1 )
2

L1 b1  a1 5 1
unde: l1   ;    1,618
2 2 2

Pasul 7 Se evaluează F(x) în punctele x1 si x2 :

Dacă F ( x1 )  F ( x 2 ) atunci a1  x*  x 2

Dacă F ( x1 )  F ( x2 ) atunci x1  x*  b1
Noul subinterval în care se găseşte optimul x* va fi:
L1
L2  ( a2 , b2 )  L1  l1 

Pasul 8 În intervalul L2 se plasează punctul x3 după relaţia:
L2
x3  a2  l2 sau x3  b2  l2 unde: l2 
2
Pasul 9 Procesul de eliminare a intervalelor se continuă astfel că la stadiul k de căutare:
xk 1  ak  lk  ak  0,382(bk  ak ) sau

Lk L1
xk 1  bk  lk  ak  0,618(bk  ak ) unde L  si Lk 
2 k  1
Pasul 10 Procedura se opreşte când este satisfăcut criteriu de convergenţă:
| xk 1  xk | eps 2

Se observă că de la pasul 5 metoda devine de optimizare a unor funcţii cu restricţii de forma: a1  x  b1 .


Punctul de optim se obţine ca media aritmetică a punctelor ultimului interval.
Desfăşurarea lucrării:
1. Metoda de mai sus va fi implementat prin construireaa unor fire de execuție la primele împărțiri
în intervale
2 Se va analiza efectul utilizării a două sau patru fire de execuție.
LABORATORUL NR.7
Testarea algoritmilor paraleli

Scop:
1. Realizarea unei analize privind creşterea complexității algoritmilor datorită acestei abordării şi analiza
impactului asupra vitezei de execuție
Considerente teoretice
PARALLEL COMPUTERS (- IN MOD UZUAL ) lucrează bazat pe:
• CUPLARE STRANSA,
• in general bazate pe SINCRONICITATE,
• CU UN SISTEM DE COMUNICATIE FOARTE RAPID SI FIABIL
• Spatiu unic de adresare (intr-o masura mare)
DISTRIBUTED COMPUTERS
• MAI INDEPENDENTE,
• COMUNICATIE MAI PUTIN FRECVENTA SI mai putin RAPIDA (ASINCRONA)
• COOPERARE LIMITATA
• NU EXISTA CEAS GLOBAL
•“Independent failures”

• Costuri ale comunicarii


– In sisteme cu transmitere de mesaje
– In sisteme cu memorie partajata
• Metrici de performanta
– Timp de Executie, Suprasarcina, Accelerare, Eficienta, Cost

Timpul de executie pentru sistemele de calcul paralel.


• Ts = Timpul de executie serial: timpul masurat de la inceputul si pana la sfarsitul executiei
algoritmului pe un calculator secvential.
• TP= Timpul de executie paralel: timpul masurat de la inceputul executiei si pana la terminarea
ultimului subtask paralel.

Suprasarcina pentru sistemele de calcul paralel.


• Suprasarcina (Total paralell overhead) To= diferenta dintre timpul total de lucru insumat al
tuturor procesoarelor si timpul necesar celui mai rapid algoritm secvential
• Timpul total de lucru insumat al tuturor procesoarelor = p*TP = timpul de calcul + timpul de
comunicare + timpul de inactivitate temporara cumulat pentru toate procesoarele

Costul pentru sistemele de calcul paralel.


• Cost = TP * p
• Costul reflecta timpul total de lucru insumat al tuturor procesoarelor
• E= Ts/Cost
• Algoritm paralel optimal in cost (cost-optimal): costul rezolvarii problemei cu ajutorul
algoritmului paralel este egal cu timpul de executie al celui mai rapid algoritm secvential
Cost=O(Ts(n))
• Eficienta unui sistem cost-optimal este O(1)

Accelerarea pentru sistemele de calcul paralel.

• Accelerarea (Speedup) = raportul dintre timpul necesar rezolvarii unei probleme pe un


procesor si timpul necesar rezolvarii in paralel pe p procesoare identice
• S este o metrica a performantei algoritmului si nu a calculatorului paralel
• Ts se refera la timpul de executie al celui mai bun algoritm secvential pentru respectiva
problema

– De exemplu, pentru un algoritm paralel de sortare, se va raporta timpul acestuia la timpul de


executia al algoritmului Quicksort secvential, nu Bubblesort secvential !

Eficienta pentru sistemele de calcul paralel.


• Eficienta utilizarii calculatorului paralel:
• E <=1, este 1 in cazul ideal S=p
• Exemplu: adunarea a n numere pe n procesoare:
• E=S/n=O(1/log n)

Legea lui Amdahl.

Legea lui Amdahl spune ca daca P este proportia din program care poate fi paralelizata si (1-P)
este proportia care nu poate fi paralelizata (ramane seriala), accelerarea maximacare poate fi
atinsa utilizand N procesoare este :
A=1/((1− P)+P/ N)
Daca facem limita cand N tinde la infinit (un numar infinit de procesoare), accelerarea
maximatinde spre 1/(1-P). In practica raportul performanta/pret scade repede odata cu
cresterea lui N chiar sidaca (1-P) are o valoare mica

Granularitatea.
• Granularitate = dimensiunea taskurilor in care e descompusa problema paralelizabila
– Granularitate fina (fine-grained): numar mare de taskuri mici
– Granularitate mare (coarse-grained): numar redus de taskuri mari
• Cresterea granularitatii (scaderea numarului de procesoare) poate ajuta la imbunatatirea
metricii de cost

Scalabilitatea.
• Performanta unui sistem paralel depinde de dimensiunea problemei si de numarul de
procesoare
• Ce se intampla daca:
– Creste dimensiunea problemei
– Creste numarul de procesoare
– Cum si cu cat se modifica performantele ?
• Tipuri de scalabilitate:
– Arhitecturala: cresterea debitului prin cresterea resurselor
– Algoritmica: abilitatea de utilizare a resurselor mai mari

Izoeficienta.
• Functia de izoeficienta: specifica rata de crestere a dimensiunii problemei necesara pentru a
mentine constanta eficienta, odata cu cresterea numarului de procesoare
• Sisteme nescalabile: nu se poate determina functia de izoeficienta (nu exista): eficienta nu
poate fi mentinuta constanta la cresterea numarului de procesoare
• Sistem eficient scalabil: functie de izoeficienta mica: ex – O(p): dimensiunea problemei
trebuie sa creasca liniar cu numarul de procesoare

• Sistem greu scalabil: functie de izoeficienta mare: ex – O(2^p): dimensiunea problemei


trebuie sa creasca exponential cu numarul de procesoare

Etapele descrierii unui algoritm paralel.


• Identificarea partilor problemei care se pot executa in paralel
• Maparea partilor pe mai multe procesoare
• Impartirea datelor (de intrare, iesire si intermediare)
• Gestiunea accesului la datele comune mai multor procesoare

Desfăşurarea lucrării:

1. Se alege o problemă numerică ce va fi implementată prin utilizarea calculului paralela


2. Se creşte treptat numărul de fire de execuție. Prin aceasta creşte şi complexitatea algoritmului.
3. Se analizează viteza de execuție pe fiecare fir în parte.
4. Se analizează viteza de execuție pe întregul algoritm
5. Se analizează impactul accesului la resurse comune.
BIBLIOGRAFIE

1. H. Georgescu,. Radu Boriga. Programare distribuită în Java / - Bucureşti : Editura Universității din
Bucureşti, 2008
2. A. Gellert, A-C. Mitea, Algoritmi Paraleli şi Distribuiți i
http://webspace.ulbsibiu.ro/arpad.gellert/html/APD.pdf
3. Potop Radu, Analiza Algoritmilor de Sortare pe Arhitecturi Paralele, 2011

S-ar putea să vă placă și