Documente Academic
Documente Profesional
Documente Cultură
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
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
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 C {
...
Execute Ob = new Execute(...);
Thread T = new Thread(Ob); // firul este creat
T.start(); // firul este pornit
...
}
Exemplul 2.
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.
#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 .....
}
unde clauzele pot fi plasate in orice ordine si se pot chiar repeta. Liniile directive lungi se pot continua pe
randul urmator cu ’\’.
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.
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:
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];
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
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.
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
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.
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ă.
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.
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();
// ------------------------------------------
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ă.
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
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.
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.
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 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);
}
}
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.
import java.util.*;
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
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ă.
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.
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.
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();
}
}
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.
Î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.
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);
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.
CyclicBarrier(int n)
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.
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:
package com.java2novice.sorting;
public class MyQuickSort {
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);
}
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
a1 x b1 .
L1 b1 a1 5 1
unde: l1 ; 1,618
2 2 2
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
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”
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
Desfăşurarea lucrării:
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