Download as pdf or txt
Download as pdf or txt
You are on page 1of 46

Rezolvarea concurenta a

problemelor

Capitolul 8

2017-2018
Inmultirea a doua matrici
• Se considera doua matrici 𝐴 si 𝐵 de dimensiuni 𝑛 × 𝑛 si se cere
determinarea matricii produs 𝐶 = 𝐴 × 𝐵 unde elementele lui 𝐶
se determina cu:
𝑛−1

𝑐𝑖𝑗 = 𝑎𝑖𝑘 × 𝑏𝑘𝑗


𝑖=0
• Se observa ca elementele lui 𝐶 se pot determina concurent.
• Vom construi cate un fir Worker parametrizat cu valorile 𝑖 si 𝑗,
responsabil cu calculul fiecarei valori 𝑐𝑖𝑗 .
• O clasa separata MMThread este responsabila cu construirea
unei matrici 𝑛 × 𝑛 de obiecte Worker care lucreaza concurent
pentru determinarea matricii 𝐶.
• Pentru testare se foloseste JUnit 4 si clasa MMThreadTest.

2017-2018
Clasa Worker
class Worker extends Thread {
int row, col;
Ce element
Worker(int row, int col) { calculeaza acest fir
this.row = row; this.col = col;
}
public void run() {
double dotProduct = 0.0;
System.out.println("Worker["+row+"]["+col+"]");
for (int i = 0; i < n; i++) {
dotProduct += a[row][i] * b[i][col];
}
c[row][col] = dotProduct;
}
}

Calculul
propriuzis
2017-2018
Clasa MMThread I
public class MMThread {
double[][] a, b, c;
int n;
public MMThread(double[][] a, double[][] b) {
n = a.length;
this.a = a;
this.b = b;
this.c = new double[n][n]; Creaza maatricea
} de fire
void multiply() {
Worker[][] worker = new Worker[n][n];
// create one thread per matrix entry
for (int row = 0; row < n; row++) {
for (int col = 0; col < n; col++) {
worker[row][col] = new Worker(row,col);
}
}
2017-2018
Clasa MMThread II
Porneste firele
// start the threads
for (int row = 0; row < n; row++) {
for (int col = 0; col < n; col++) {
worker[row][col].start();
}
}
// wait for them to finish
for (int row = 0; row < n; row++) {
for (int col = 0; col < n; col++) {
try {
worker[row][col].join();
} catch (InterruptedException ex) {
ex.printStackTrace(); Asteapta sa se
} termine
}
}
}
} 2017-2018
Clasa MMThreadTest
import org.junit.Test;
import org.junit.Assert;
public class MMThreadTest {
public MMThreadTest() { }
@Test public void testRun() {
System.out.println("run");
double[][] a = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
double[][] b = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
MMThread instance = new MMThread(a,b);
instance.multiply();
double[][] c = instance.c;
int n = a.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
Assert.assertEquals(new Double(c[i][j]),
new Double(a[i][j]));
}
}
}
} 2017-2018
Dezavantaje ale abordarii naive
• Aparent solutia naiva maximizeaza concurenta.
• Pentru matrici mari se creaza foarte multe fire.
• Gestiunea firelor presupune costuri suplimentare:
– Memorie suplimentara pentru fiecare fir in parte
– Efort de calcul suplimentar pentru:
• Crearea
• Planificarea
• Distrugerea firelor
– Raportul munca utila / effort suplimentar este scazut

• Crearea unui numar mare de fire avand fiecare o viata


foarte scurta (adica se executa doar pentru putin timp) este
o metoda ineficienta de a organiza o aplicatie de calcul
concurent ! 2017-2018
Rezerve de fire
• O metoda mai eficienta de a organiza o aplicatie de
calcul concurent este de a crea o rezerva de fire (engl.
thread pool).
• Firele din rezerva reprezinta astfel resurse durabile
(engl. long-lived resources) de calcul ele putand fi
(re)alocate in mod repetitiv unor sarcini de calcul de
scurta durata.
• Rezerva de fire evita costul suplimentar de creare /
distrugere repetata de fire. Acesta poate fi semnificativ
in cazul in care cererea de calcul contine sarcini multe
si de scurta durata.

2017-2018
Rezerve de fire = abstractizare

• Pe platformele multi-procesor rezervele de fire pot fi


dependente de platforma in scopul eficientizarii
implementarii.
• O rezerva de fire ofera un beneficiu dpdv al
modularizarii aplicatiei:

Programatorul este degrevat de cunoasterea detaliilor


specifice platformei, programul putand rula pe
diverse platforme uni- respectiv multi-procesor.

2017-2018
Sarcini de lucru in Java
• O sarcina de lucru ce NU intoarce nici un rezultat se
reprezinta printr-un obiect Runnable.

public interface Runnable {


void run();
}

• O sarcina de lucru ce intoarce un rezultat generic de tip


𝑇 se reprezinta printr-un obiect Callable<T>.

public interface Callable<T> {


T call() throws Exception;
}
2017-2018
Executori
• O aplicatie complexa este descompusa in sarcini logice
de calcul (engl. execution tasks). Obiectele ce
abstractizeaza executia sarcinilor unei aplicatii se
numesc executori si se implementeaza prin interfata
Executor.
• In aplicatiile complexe se urmareste separarea
gestiunii firelor de logica aplicatiei. Firele ofera un
mecanism de executie concurenta si asincrona a
sarcinilor. Firele pot fi privite ca resurse disponibile
pentru executia sarcinilor.

2017-2018
Exemple de executori
• Spre exemplu, putem defini doua modalitati simple de
alocare a sarcinilor pe fire de executie:

– Alocarea secventiala, cand toate sarcinile se aloca unui


singur fir de executie. Are dezavantajul ca nu foloseste
optim resursele disponibile si ofera un timp de raspuns
necorespunzator si o reactivitate scazuta

– Alocarea sarcina-pe-fir, cand fiecare sarcina se aloca pe un


fir separat. Am vazut ca aceasta metoda sufera de problema
unei gestiuni necorespunzatoare a resurselor diponibile,
deoarece fragmenteaza excesiv aceste resurse.
2017-2018
Interfata Executor I
• Abstractizeaza executia unei sarcini de executie in Java.
public interface Executor {
void execute(Runnable command);
}

• Permite executia asincrona a sarcinilor permitand o varietate


de politici de executie. Decupleaza activitatea de trimitere a
unei sarcini spre executie (engl. task submission) de executia
propriuzisa a sarcinii (engl. task execution).
• Executorii se bazeaza pe sablonul producator-consumator.
– O activitate care trimite sarcini spre executie este un producator de
unitati de lucru,
– Firele care in final executa sarcinile sunt consumatori. Executia unei
sarcini este abstractizata ca activitate de consumare a unei sarcini.
2017-2018
Interfata Executor II
• Daca r este un obiect Runnable ce descrie o sarcina de
executie, in loc de a asocia direct sarcina unui fir astfel:
(new Thread(r)).start();

se va folosi un obiect e ce implementeaza interfata Executor:


e.execute(r);

• A doua metoda nu expliciteaza modul in care se va aloca un fir


lucrator (engl. worker thread) sarcinii r si astfel nu se creaza o
legatura directa intre sarcina si firul alocat.
• Politica alocarii firului lucrator nu este specificata explicit.
Decuplarea trimiterii unei sarcini de executia sa permite
schimbarea relativ usoara a politicii de executie a unei
multimi de sarcini. 2017-2018
Politici de executie
• O politica de executie (engl. execution policy) specifica
ce, unde, cand si cum se va executa o sarcina:
– In ce fir se va executa sarcina respectiva ?
– In ce ordine se vor executa sarcinile: FIFO, LIFO, prioritati, etc ?
– Cate sarcini se pot executa concurent ?
– Cate sarcini pot fi pastrate in coada inainte de inceperea executiei
?
– Daca executia unei sarcini trebuie sa fie rejectata deoarece
sistemul este supraincarcat atunci ce sarcina va fi aleasa drept
victima si cum trebuie aplicatia instiintata de acest lucru ?
– Ce actiuni trebuie intreprinse inainte / dupa executia unei sarcini ?

2017-2018
Politica optimala

• Politica optimala depinde de:


– resursele de calcul existente
– cerintele de calitate a serviciului.

• De exemplu, prin limitarea numarului de executii


concurente ne putem asigura de faptul ca:
– aplicatia nu va esua datorita epuizarii resurselor
– nu isi va degrada necorespnzator perfomantele prin
epuizarea resurselor, datorita fragmentarii exagerate a
cestora

2017-2018
Exemple de politici simple de executie
• Politica de a crea un nou fir pentru fiecare sarcina alocata, de a aloca
imediat sarcina acestui fir si apoi a-l lansa in executie se poate
implementa astfel:

public class ThreadPerTaskExecutor implements Executor {


public void execute(Runnable r) {
(new Thread(r)).start();
};
}

• Executarea unei sarcini in firul apelantului se poate specifica astfel:

public class WithinThreadExecutor implements Executor {


public void execute(Runnable r) {
r.run();
};
}
2017-2018
Executori pentru rezerve de fire in Java
• Java dispune de un executor special reprezentat prin
interfata java.util.ExecutorService pentru:
– Implementarea unei rezerve de fire
– Executia asincrona a unei multimi de sarcini

public interface ExecutorService extends Executor {


<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
}

2017-2018
Executia asincrona a sarcinilor
• Orice sarcina ce trebuie trimisa spre executie la un
executor are un ciclu de viata ce contine fazele:
– creare,
– trimitere,
– startare,
– terminare.

• Executia sarcinii dureaza un timp nenul.

• Executia are loc in mod asincron (concurent) cu firul


apelant.

2017-2018
Interfata Future<T>
• Pentru reprezentarea ciclului de viata al sarcinii se foloseste un
obiect Future<T>. Interfata Future este definita in pachetul
java.util.concurrent astfel:

public interface Future<T> {


boolean cancel(boolean mayInterruptIfRunning);
T get() throws InterruptedException, ExecutionException,
CancellationException;
boolean isCancelled();
boolean isDone();
}
Intoarce rezultatul sarcinii.
Apelul blocheaza apelantul daca rezultatul nu este gata.

• Un Future care nu intoarce o valoare este reprezentat prin


Future<?> 2017-2018
Interfete functionale si Lambda expresii
• O interfata ce contine o singura metoda = interfata functionala.
• Exemple: Runnable, Callable.
• Java permite definirea facila a obiectelor ce implementeaza interfete
functionale folosind Lambda expresii.
public class RunnableTest {
public static void main(String[] args) {
System.out.println("=== RunnableTest ===");
// Anonymous Runnable
Runnable r1 = new Runnable(){
@Override public void run(){
System.out.println("Hello world one!");
}
};
// Lambda Runnable
Runnable r2 = () -> System.out.println("Hello world two!");
// Run them!
r1.run();
r2.run();
}
} 2017-2018
Crearea rezervelor de fire
• O rezerva de fire este accesibila printr-un obiect executor ce
implementeaza interfata ExecutorService:
– ThreadPoolExecutor
– ScheduledThreadPoolExecutor
• Crearea unei rezerve de fire se poate realiza:
– Varianta simpla: folosind metodele factory ale clasei Executors, care
peermite crearea unor rezervee de fire preconfigurate.
– Varianta complicata: folosind constructorii claselor
ThreadPoolExecutor si ScheduledThreadPoolExecutor
• Exemple:
ExecutorService executorService1 =
Executors.newSingleThreadExecutor();
ExecutorService executorService2 =
Executors.newFixedThreadPool(10);
ExecutorService executorService3 =
Executors.newScheduledThreadPool(10);
2017-2018
Rezerve de fire predefinite
public static ExecutorService newFixedThreadPool(int nThreads)
Creaza o rezerva de fire avand un numar dat nThreads de fire organizate
intr-o coada circulara. Firele sunt create la trimiterea sarcinilor. Daca se
trimit sarcini peste numarul maxim de fire active, sarcinile in plus vor
astepta intr-o coada pana la eliberarea unui fir. Daca un fir se termina
prematur, inainte de a se incheia executia unei sarcini, se va crea un alt fir
(doar daca este necesar).
public static ExecutorService newCachedThreadPool()
Creaza o rezerva de fire ce va crea fire pe masura ce acest lucru este cerut
de aplicatie. Firele existente se refolosesc ori de cate ori este nevoie, altfel
se creaza un nou fir. Firele care nu sunt folosite timp de 1 minut sunt
terminate si eliminate. O rezerva ce nu este folosita timp indelungat nu va
consuma resurse suplimentare.
public static ExecutorService newSingleThreadExecutor()
Creaza un executor pentru o rezerva de fire cu un singur fir lucrator,
aceata putand fi inlocuit daca se termina prematur. Se garanteaza executia
secventiala a sarcinilor, pe masura sosirii lor. 2017-2018
Adunarea matricilor folosind divide-et-impera
• Se considera ca matricile sunt patrate de dimensiune 𝑛 = 2𝑘 .
Orice matrice 𝐴 ∈ 𝑛 × 𝑛 se descompune astfel:
𝐴00 𝐴01
𝐴=
𝐴10 𝐴11
𝑛 𝑛
unde matricile 𝐴𝑖𝑗 ∈ × = 2𝑘−1 × 2𝑘−1
2 2
• Adunarea de matrici 𝐶 = 𝐴 + 𝐵 se descompune astfel:
𝐶00 𝐶01 𝐴00 𝐴01 𝐵00 𝐵01
= + =
𝐶10 𝐶11 𝐴10 𝐴11 𝐵10 𝐵11
𝐴00 + 𝐵00 𝐴01 + 𝐵01
𝐴10 + 𝐵10 𝐴11 + 𝐵11
• Rezulta ca cele 4 sume 𝐴𝑖𝑗 + 𝐵𝑖𝑗 se pot realiza concurent.
2017-2018
Reprezentarea matricilor
• O matrice se reprezinta prin clasa Matrix ce contine:
– Dimensiunea reprezentata prin campul dim;
– Deplasamantul pe linii si pe coloane al elementului din stanga sus al
matricii: rowDisplace si colDisplace. Aceste valori sunt necesare
pentru a accesa elementele matriclor 𝐴10 si 𝐴11 .
– Un tablou bidimensional data cu elementele matricii
• Exista doi constructori:
– Crearea unei matrici 𝑑 × 𝑑
– Crearea si initializarea unei matrici pe baza unui tablou bidimensional
• O matrice dispune de metode pentru:
– Determinarea dimensiunii getDim()
– Citirea / scrierea unui element (row,col); acesta se afla pe linia
row+rowDisplace si pe coloana col+colDisplace.
• O matrice se descompune folosind metoda split() in 4
submatrici patrate de dimensiuni egale. 2017-2018
Clasa Matrix I
private static class Matrix {
int dim;
double[][] data;
int rowDisplace, colDisplace;
Matrix(int d) {
dim = d;
rowDisplace = colDisplace = 0;
data = new double[d][d];
}
Matrix(double[][] matrix, int x, int y, int d) {
data = matrix;
rowDisplace = x; colDisplace = y;
dim = d;
}
double get(int row, int col) {
return data[row+rowDisplace][col+colDisplace];
}
void set(int row, int col, double value) {
data[row+rowDisplace][col+colDisplace] = value;
}
2017-2018
Clasa Matrix II
int getDim() { return dim; }

Matrix[][] split() {
Matrix[][] result = new Matrix[2][2];
int newDim = dim / 2;
result[0][0] = new Matrix(data, rowDisplace, colDisplace,
newDim);
result[0][1] = new Matrix(data, rowDisplace,
colDisplace + newDim, newDim);
result[1][0] = new Matrix(data, rowDisplace + newDim,
colDisplace, newDim);
result[1][1] = new Matrix(data, rowDisplace + newDim,
colDisplace + newDim, newDim);
return result;
}
}

2017-2018
Realizarea adunarii
• Task-ul de adunare AddTask primeste matricile: operanzii a si b,
respectiv rezultatul c. Fie n dimensiunea operanzilor.
• Daca n = 1 atunci matricile sunt scalari si adunarea este scalara.
• Daca n > 1 atunci se descompun matricile aa, bb, si cc.
• Se realizeaza apoi adunarea concurent, pe fiecare bloc, folosind
task-ul AddTask, pentru fiecare bloc preluat din aa, bb si cc.
• Pentru realizarea calculelor se foloseste o rezerva de fire.
• AddTask este o clasa separata ce implementeaza interfata
Runnable.
• Obtinerea rezultatului foloseste o matrice de obiecte Future<?>.
• Matricea este creata separat, apoi este initializata in urma
operatiei submit() de trimitere a sarcinilor spre executie.
Asteptarea terminarii calculelor se realizeaza invocand metoda
get() a clasei Future. 2017-2018
Clasa MatrixTask
import java.util.concurrent.*;

public class MatrixTask {


static ExecutorService exec = Executors.newCachedThreadPool();

static Matrix add(Matrix a, Matrix b)


throws InterruptedException, ExecutionException {
int n = a.getDim();
Matrix c = new Matrix(n);
Future<?> future = exec.submit(new AddTask(a, b, c));
future.get();
return c;
}

static class AddTask implements Runnable {


// ...
}
}

2017-2018
Clasa AddTask
static class AddTask implements Runnable {
Matrix a, b, c;
public AddTask(Matrix a, Matrix b, Matrix c) {
this.a = a; this.b = b; this.c = c;
}
public void run() {
try {
int n = a.getDim();
if (n == 1) {
c.set(0, 0, a.get(0,0) + b.get(0,0));
} else {
Matrix[][] aa = a.split(), bb = b.split(), cc = c.split();
Future<?>[][] future = (Future<?>[][]) new Future[2][2];
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
future[i][j] = exec.submit(new AddTask(aa[i][j],
bb[i][j], cc[i][j]));
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++) future[i][j].get();
}
} catch (Exception ex) { ex.printStackTrace(); }
}
}
2017-2018
Inmultirea matricilor folosind divide-et-impera
• Inmultirea de matrici 𝐶 = 𝐴 × 𝐵 se descompune astfel:

𝐶00 𝐶01 𝐴00 𝐴01 𝐵00 𝐵01


= ×
𝐶10 𝐶11 𝐴10 𝐴11 𝐵10 𝐵11
𝐴00 × 𝐵00 + 𝐴01 × 𝐵10 𝐴00 × 𝐵01 + 𝐴01 × 𝐵11
=
𝐴10 × 𝐵00 + 𝐴11 × 𝐵10 𝐴10 × 𝐵01 + 𝐴11 × 𝐵11

• Cele 8 produse 𝐴𝑖𝑗 × 𝐵𝑘𝑙 se pot realiza in mod concurent.


Apoi, cele 4 sume 𝐴𝑖0 × 𝐵0𝑗 + 𝐴𝑖1 × 𝐵1𝑗 se pot realiza in
paralel.

• Tema: sa seimplementeze clasa MulTask, dupa modelul clasei


AddTask, care realizeaza inmultirea celor doua matrici.
2017-2018
Determinarea numerelor lui Fibonacci
• Pentru exemplificarea sarcinilor ce intorc o valoare folosind
Callable<T>, consideram calculul termenilor sirului Fibonacci:
1 if 𝑛 = 0
𝐹𝑛 = 1 if 𝑛 = 1
𝐹𝑛−1 + 𝐹𝑛−2 if 𝑛 ≥ 2

• Se creaza o sarcina FibTask care implementeaza interfata


Callable<Integer>.

• Aceasta abordare de calcul a numerelor lui Fibonacci este


foarte ineficienta !
• Tema: De ce?
• Tema: Sa se realizeze o implementare eficienta.
2017-2018
Clasa FibTask
import java.util.concurrent.*;

public class FibTask implements Callable<Integer> {


static ExecutorService exec = Executors.newCachedThreadPool();
int arg;
public FibTask(int n) {
arg = n;
}
public Integer call() {
try {
if (arg > 2) {
Future<Integer> left = exec.submit(new FibTask(arg-1));
Future<Integer> right = exec.submit(new FibTask(arg-2));
return left.get() + right.get();
} else {
return 1;
}
} catch (Exception ex) {
ex.printStackTrace();
return 1;
}
}
}
2017-2018
Analiza concurentei
• Un calcul multifir se poate vizualiza grafic pe un graf orientat
aciclic.
• Nod = pas de calcul (orice instructiune este un pas de calcul).
• Arc = o dependenta intre un nod predecesor si un nod succesor.
• Un fir este practic o secventa de noduri, astfel incat fiecare
instructiune a sa este un pas separat de calcul. Fiecare nod
depinde de predecesorul sau din secventa.
• Un nod care creaza un obiect Future are 2 succesori:
– Un nod succesor in acelasi fir
– Un nod succesor care este primul nod asociat sarcinii de calcul
asincrone corespunzatoaare obiectului Future.
• Pentru fiecare operatie get() a unui obiect Future se creaza un
arc de la ultimul nod al sarcinii asincrone asociate obiectului
catre nodul corespunzator invocarii operatiei get(). 2017-2018
Graful de dependenta pentru FibTask

Sursa: Herlithy & Shavit, 2012

• Se observa ca graful de dependente mimeaza arborele de apeluri recursive


pentru calculul termenului lui Fibonacci de ordin n.
• Diferenta fata de implementarea secventiala este faptul ca sarcinile
determinate de subarborii corespunzatori calculului termenilor 𝐹𝑛−1 si
𝐹𝑛−2 se executa concurent, in fire separate.
2017-2018
Timp de executie
• Fie 𝑇𝑁 timpul minim necesar executarii unui program
concurent pe un sistem cu 𝑁 procesoare.

• 𝑇𝑁 este o valoare ideala, fiind practic o margine


inferioara pentru timpul real de executie al unui
program concurent.

• 𝑇1 este timpul de executie pe un singur procesor –


computation’s work

• 𝑇∞ = lungimea caii critice (engl. critical path length)


2017-2018
Factorul de accelerare
• Factorul de accelerare (engl. speedup) pentru executia pe 𝑁
procesoare:
𝑇1
𝑆𝑁 =
𝑇𝑁
• Daca o fractiune 𝑝 ∈ [0,1] din program se poate executa
concurent atunci:
𝑝
𝑇𝑁 = 1 − 𝑝 +
𝑁
1 𝑁
𝑆𝑁 = 𝑝 =1+ 𝑁−1 × 1−𝑝 ≤𝑁
1−𝑝+
𝑁
𝑇
𝑇𝑁 ≥ 1 si 𝑇𝑁 ≥ 𝑇∞
𝑁
• Definitie. Un program concurent are accelerare liniara (engl.
linear speedup) dnd 𝑆𝑁 = Θ 𝑁 .

2017-2018
Analiza operatiilor concurente cu matrici: cazul ideal
• Fie 𝐴𝑁 𝑛 numarul de pasi necesar adunarii a doua matrici
𝑛 × 𝑛 pe 𝑁 procesoare.

𝑛
𝐴1 𝑛 = 4 × 𝐴1 + Θ 1 = Θ(𝑛2 ) De ce?
𝑛 2
𝐴∞ 𝑛 = 𝐴∞ + Θ 1 = Θ(log 𝑛) De ce?
2

• Tema: Fie 𝑀𝑁 𝑛 numarul de pasi necesar inmultirii a doua


matrici 𝑛 × 𝑛 pe 𝑁 procesoare. Se cere:
𝑀1 𝑛 =?
𝑀∞ 𝑛 =?

2017-2018
Analiza concurentei pe sisteme multiprocesor reale I
• Sistemele de operare actuale permit descompunerea unei aplicatii intr-o
multime de fire la nivel de aplicatie / utilizator.
• Nucleul sistemului de operare dispune de un planficator care gestioneaza
alocarea si executia firelor pe procesoarele fizice ale sistemului.
• Din punctul de vedere al dezvoltatorului de programe, o aplicatie
concurenta este conforma unui model pe trei niveluri:
– Nivelul logic, la care aplicatia este descompusa intr-o multime de sarcini (engl.task)
– Nivelul intermediar, la care un planificator (la nivel de utilizator) planifica si aloca
aceste sarcini pe un numar finit de fire la nivel de utilizator
– Nivelul fizic, la care planificatorul din nucleul sistemului de operare planifica si
aloca firele utilizator pe procesoarele sistemului
• La un moment dat, pe un sistem cu N procesoare, un numar 0 ≤ 𝑛𝑖 ≤ 𝑁
fire utilizator sunt alocate de nucleul SO pentru a executa concurent cate
un pas de calcul. Numarul mediu de procesoare disponibile pentru a
executa concurent cate un pas de calcul la fiecare moment de timp pe un
interval de 𝑇 pasi este:
1 𝑇−1
𝑁𝐴 = ×
𝑇 𝑖=0 𝑛𝑖 . 2017-2018
Analiza concurentei pe sisteme multiprocesor reale II
• Se urmareste obtinerea unei accelerari avand ca valoare media 𝑁𝐴 ≤ 𝑁.
• O planificare este greedy dnd numarul de pasi executati la fiecare moment
de timp 𝑖 este egal cu minimul dintre numarul de procesoare disponibile
𝑛𝑖 si numarul de noduri gata de executie (noduri al caror pas curent este
gata de executie) din graful programului.
• Teorema. Orice program concurent avand efortul de calcul 𝑇1 , lungimea
caii critice 𝑇∞ ce are la dispozitie 𝑁 fire utilizator se va executa pe orice
planificare greedy intr-un timp:
𝑇1 𝑇∞ × 𝑁−1
𝑇≤ +
𝑁𝐴 𝑁𝐴
• Fiecarui procesor alocat i se asociaza un jeton virtual. In total se vor aloca
pentru executia greedy a programului un numar de 𝑇−1 𝑖=0 𝑛𝑖 jetoane. La
fiecare pas cel putin un jeton corespunde unui pas executabil. Numarul
total de jetoane executabile alocate este 𝑇1 . Numarul total de jetoane
alocate unor fire ce nu sunt imediat executabile este cel mult lungimea
caii critice 𝑇∞ inmultit cu numarul maxim de procesoare disponibile
(excluzandu-l pe cel alocat cel putin unui fir imediat executabil), adica
𝑁 − 1. De aici teorema rezulta in mod trivial. 2017-2018
Discutie
• In cazul optim avem 𝑁𝐴 = 𝑁. Rezulta ca:
𝑇1
𝑇 ≤ + 𝑇∞
𝑁
• Exista urmatorul argument intuitiv. Daca in fiecare pas de calcul sunt
disponibile 𝑁 procesoare, atunci cei 𝑇 pasi de timp cuprind:
𝑇
– Pasi completi, cand se folosesc toate procesoarele. Sunt cel mult 1 astfel de pasi
𝑁
– Pasi incompleti, cand nu se folosesc toate procesoarele, dar se foloseste sigur cel
putin 1 procesor si lungimea caii critice se reduce cu 1 (avand o strategie greedy)
• De aici, adunand cele doua valori, marginea superioara a lui 𝑇 rezulta in
mod evident.
𝑇
• Observatie. Marginea superioara 1 + 𝑇∞ a oricarei planificari greedy este
𝑁
cel mult egala cu dublul timpului optim de planificare a executiei
programului pe 𝑁 procesoare.
𝑇
• Fie 𝑇𝑁∗ timpul optim. Avem 𝑇𝑁∗ ≥ 1 si 𝑇𝑁∗ ≥ 𝑇∞ , de unde concluzia
𝑁
corolarului decurge trivial.
2017-2018
Distributia sarcinilor
• Pentru obtinerea unei accelerari convenabile firele trebuie
planificate a.i. sa lucreze (sa fie ocupate) in permanenta,
planificarea rezultata fiind astfel cat mai “greedy” posibil.
• Este necesara o strategie (algoritm) de distributie a sarcinilor
(engl. work distribution algorithm) care sa lucreze eficient
pentru alocarea sarcinilor gata de executie firelor disponibile.
• Exista doua mari strategii:
– Pasarea sarcinilor (engl. work dealing). Un fir supraincarcat incearca
sa “paseze” sarcinile suplimentare altor fire mai putin incarcate.
Dezavantajul este ca daca toate firele sunt supraincarcate se va
consuma fara rost un efort suplimentar de calcul pentru redistribuire.
– Preluarea sarcinilor (engl. work stealing). Un fir subincarcat preia
(“fura”) sarcini de la alte fire mai incarcate. Metoda are avantajul ca
firele supraincarcate nu vor consuma timp in mod inutil pentru
redistribuirea sarcinilor. 2017-2018
Preluarea sarcinilor
• Pentru implementarea acestei strategii fiecare fir va gestiona o coada cu
doua capete (engl. double-ended queue – dequeue) numita DEQueue ce
ofera metodele:
– pushBottom(), popBottom() pentru adaugarea si extragerea unei sarcini de la capatul
din spate al cozii
– popTop() pentru extragerea unei sarcini de la capatul din fata al cozii.
• Daca un fir necesita o sarcina atunci o preia cu popBottom() din propria
coada.
• Daca un fir descopera ca nu mai are sarcini, coada sa fiind goala, atunci el
alege la intamplare un alt fir “victima” si apeleaza metoda popTop() a
cozii acestui fir in speranta de a prelua (“fura”) o sarcina de acolo.

• Observatii si intrebari:
– Clasa DEQueue nu necesita metoda pushTop(). De ce?
– De ce nu se foloseste o coada simpla in locul dequeue?
– Cand se apeleaza metoda pushBottom()?
2017-2018
Clasa WorkStealingThread
import java.util.Random;
public class WorkStealingThread {
DEQueue[] queue;
int me;
Random random;
public WorkStealingThread(DEQueue[] queue) {
this.queue = queue; this.random = new Random();
}
public void run() {
int me = ThreadID.get();
Runnable task = queue[me].popBottom(); // pop first task
while (true) {
while (task != null) { // if there is a task
task.run(); // execute it and then
task = queue[me].popBottom(); // pop the next task
}
while (task == null) { // steal a task
Thread.yield();
int victim = random.nextInt() % queue.length;
if (!queue[victim].isEmpty()) {
task = queue[victim].popTop();
}
}
} 2017-2018
Observatii asupra clasei WorkStealingThread
• Clasa descrie o posibilitate de a implementa un fir intr-un executor bazat
pe tehnica preluarii sarcinilor.
• Toate firele partajeaza un vector de DEQueue, cate o coada pentru fiecare
fir in parte.
• Implementarea are urmatoarea problema: procesul de preluare a sarcinilor
continua la infinit chiar si daca cozile firelor sunt goale.
• Observatie. Incepand cu Java 1.8, exista deja o clasa executor care
implementeaza tehnica preluarii sarcinilor:
public static ExecutorService newWorkStealingPool(int parallelism)
Creaza o rezerva de fire ce foloseste tehnica preluarii sarcinilor astfel
incat sa mentina un nivel de paralelism impus. Nivelul de paralelism
reprezinta numarul maxim fire care pot fi angajate sau potential
angajabile in executia sarcinilor. Daca parallelism lipseste atunci se
foloseste un nivel de paralelism implicit corespunzator numarului de
procesoare disponibile.
2017-2018
Carte de concurenta in Java
• Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes,
Doug Lea, Java Concurrency in Practice,, Addison-Wesley
Professional, 2006
http://jcip.net/

2017-2018

You might also like