Sunteți pe pagina 1din 9

FIRE DE EXECUTARE

[H.G: 9.1-9.5]

Conceptual, se presupune prezenţa pe sistemul gazdă a mai multor


procesoare.
Pentru prelucrarea separată a fiecărei acţiuni, va fi lansat un fir de
executare, care va prelua sarcina de a executa acţiunea respectivă; în cazul ideal,
acela în care există un procesor liber, această sarcină este materializată de un astfel
de procesor. În cazul existenţei unui singur procesor, sau (mai general) atunci când
numărul de acţiuni ce trebuie executate concomitent depăşeşte numărul de
procesoare libere, executarea concurentă este simulată conform modelului aleator.

Chiar dacă aplicaţia nu apelează la fire de de executare, în fapt există două


astfel de fire: cel curent şi cel folosit de colectorul de reziduuri.

Crearea firelor de executare

• Clasa Thread

O primă modalitate de a crea şi lansa în executare fire (de executare) este


de a folosi clasa Thread din pachetul java.lang:
public class Thread extends Object implements Runnable
unde despre interfaţa Runnable vom vorbi mai jos.

Pentru a crea un fir de executare, trebuie început prin a crea un obiect de


tipul Thread:
Thread fir = new Thread();
După ce obiectul este creat, se poate trece la executarea sa. Când firul este
gata de executare, se invocă metoda sa start fără argumente. Ca urmare firul de
executare începe să execute metoda sa run.
Când metoda run se încheie, firul de executare se termină.
Prezentăm în continuare schema generală a stărilor în care se poate afla un
proces (fir de executare).

Creat Gata de În curs de Terminat


executare executare

Blocat
Un fir (de executare) este gata de executare dacă îndeplineşte toate
condiţiile pentru a se trece la executarea sa, dar nu i s-a alocat încă un procesor;
când un procesor liber preia firul, acesta trece în starea în curs de executare.
Blocarea unui fir de executare poate fi realizată pe baza unei condiţii (de exemplu
un semafor); ca urmare firul trece în starea blocat. La deblocarea sa, realizată prin
mecanisme dintre care unele vor fi prezentate în continuare, el revine în starea gata
de executare, aşteptând ca un procesor să devină liber şi să reia executarea sa.

Metoda run din clasa Thread nu prevede vreo acţiune. De aceea trebuie
ca programatorul să extindă clasa Thread şi să rescrie (suprapună) metoda run,
precizând acţiunea dorită.

Se impun câteva precizări privind firele de executare.


Firul principal de executare foloseşte ca punct de plecare clasa principală
(cea care conţine metoda main) şi "ştie" că trebuie să execute instrucţiunile din
metoda main.
Un fir de executare este lansat de un obiect ce instanţiază o clasă ce extinde
clasa Thread. Fie Tip numele clasei şi Ob numele obiectului. Lansarea se face
prin apelul Ob.start() .
Firul foloseşte (ca şi firul principal) programul curent, cu deosebirea că
acum "clasa principală" este considerată a fi instanţierea clasei Tip reprezentată
prin obiectul Ob. În plus, firul de executare ştie că trebuie să execute instrucţiunile
din metoda "principală" run a clasei Tip.
Să presupunem că în clasa Tip mai apare şi o metodă met. Trebuie
clarificată diferenţa între invocările Ob.start() şi Ob.met(...). Invocarea
Ob.met(...)are următorul efect: obiectul Ob este "pus" să execute metoda met şi
nu se trece mai departe decât după ce această activitate a fost încheiată. În schimb,
invocarea Ob.start() realizează lansarea unui fir de executare pe baza obiectului
Ob; deci obiectul Ob este folosit ca model de plecare pentru noul fir de executare şi
nu este "pus" să execute ceva. În continuare noul fir de executare îşi desfăşoară
activitatea concurent cu firul care l-a lansat. Obiectul Ob nu este "ocupat" cu vreo
activitate, deci în continuare el poate executa acţiuni. De exemplu obiectul poate fi
pus să execute o metodă sau să modifice un câmp al clasei a cărei instanţiere este.
De asemenea el poate influenţa activitatea firului de executare lansat pe baza lui;
această posibilitate este folosită frecvent pentru a controla terminarea firelor de
executare.

Exemplul 1. Să considerăm următorul program:


class Tip extends Thread {
int k; boolean continua=true;
public void run() {
while(continua) IO.write(" " + k++);
IO.writeln("Fir");
}
public void terminare() { continua=false; }
}
class P1 {
public static void main(String[] w)
Tip Ob = new Tip(); Ob.start();
while ( IO.readch() != '\n' );
Ob.terminare(); IO.writeln("main");
}
}
Pe baza obiectului Ob este lansat un fir de executare. Activitatea acestuia
constă în a tipări, atâta timp cât variabila booleană continua are valoarea true,
şirul numerelor naturale. Firul de executare principal prevede citirea repetată de
caractere, până când este introdus <Enter>; în acel moment obiectul Ob execută
metoda terminare, al cărei efect este modificarea valorii lui continua în
false, ceea ce va determina oprirea firului de executare lansat pe baza obiectului
Ob. Firul de executare principal se termină şi el. Să remarcăm că nu este previzibil
dacă mesajul "Fir" va fi tipărit înainte sau după mesajul "main".

• Interfaţa Runnable
Interfaţa Runnable apare în pachetul java.lang şi este declarată prin:
public interface Runnable
şi anunţă doar metoda:
public void run()

Utilizarea interfeţei Runnable constituie o alternativă la extinderea clasei


Thread. Avantajul constă în primul rând în însuşi faptul că este o interfaţă: o clasă
oarecare poate implementa Runnable şi extinde o altă clasă (pe când o clasă ce
extinde Thread nu mai poate extinde vreo altă clasă).
Modul tipic de utilizare este următorul:
class Execut implements Runnable {
. . .
public void run() { . . . }
. . .
}
class Clasa {
. . .
Execut Ob = new Execut(...);
Thread Fir = new Thread(Ob); // *
Fir.start(); // **
. . .
}

prin care este creat (*) şi apoi lansat (**) firul de executare Fir. Observăm că a
fost folosit constructorul cu un argument, al cărui tip este o clasă ce implementează
interfaţa Runnable.

Exemplul 2. Pentru ilustrarea celor de mai sus, reluăm Exemplul 1,


utilizând interfaţa Runnable în loc de clasa Thread.
class Execut implements Runnable {
int k; boolean continua=true;
public void run() {
while (continua) IO.write(" " + k++);
IO.writeln("Fir");
}
public void terminare() { continua = false; }
}
class RunFir {
public static void main(String[] arg) {
Execut Ob = new Execut();
Thread fir = new Thread(Ob);
fir.start();
while ( IO.readch() != '\n');
Ob.terminare(); IO.writeln("main");
}
}

cu observaţia că metoda terminare a fost invocată prin intermediul obiectului Ob


(este greşit să folosim fir pentru această invocare). Apare deci clară distincţia
între un fir de executare şi obiectul pe baza căruia a fost lansat.

Clasa Thread din pachetul java.lang conţine şi metoda:


static void sleep(long milis)

Un prim mod de sincronizare


Vom prezenta în acest paragraf un prim mod de sincronizare, cu largă
aplicabilitate în transcrierea în Java a algoritmilor paraleli.
Să ne situăm în cazul în care un fir de executare lansează unul sau mai
multe fire de executare care, prin natura problemei, trebuie să aştepte terminarea
activităţilor acestora înainte de a întreprinde o nouă acţiune (în lipsa unei astfel de
măsuri, firul principal îşi continuă activitatea în paralel cu firele pe care le-a
lansat).
Pentru rezolvarea problemei poate fi folosită metoda join a clasei
Thread. Ea are următoarele două forme:
public final void join() throws InterruptedException
public final void join(long mili) throws InterruptedException
Prima formă face ca firul de executare care a lansat la rândul său un fir de
executare să aştepte terminarea acestuia un timp nelimitat, înainte de a trece la o
următoarea acţiune prevăzută în program. A doua formă limitează aşteptarea la un
număr specificat de milisecunde.

Exemplul 3. Să considerăm următorul program:


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[] sir)
throws InterruptedException {
Tip Ob = new Tip(); Ob.start();
Thread.sleep(50);
Ob.k = 2; Ob.join();
IO.writeln("***");
}
}

După ce lansează un nou fir de executare pe baza obiectului Ob, firul


principal îşi întrerupe activitatea 50 milisecunde, apoi modifică în 2 valoarea
câmpului k al clasei Tip şi aşteaptă ca acesta să îşi încheie activitatea, înainte de a
tipări trei asteriscuri şi de a îşi termina la rândul său executarea. Drept urmare la
ieşire va apare un şir de 1, urmat de un şir de 2.

Firul principal de executare nu este cu nimic deosebit de cele pe care le


lansează; în particular, aceasta înseamnă că firul principal, după ce lansează un fir,
îşi poate încheia activitatea (se poate termina), întreaga aplicaţie încheindu-se
atunci când firul nou lansat se termină. Afirmaţia de mai sus este valabilă pentru
orice fir (nu neapărat cel principal) ce lansează alte fire.

Încercare nereuşită pentru problema excluderii


reciproce
Lansăm n fire de executare care prevăd fiecare mărirea de un număr dat de
ori a unei variabile comune total. Pentru a simula modul în care se realizează
efectiv incrementarea lui total cu o unitate (este utilizat un registru), este folosită
variabila suplimentară temp. În plus, pentru a accentua caracterul nedeterminist,
este folosită metoda delay cu zero argumente ce amână cu (cel puţin) un număr de
milisecunde generat aleator, executarea următoarei instrucţiuni; în acest scop este
folosită metoda statică random din pachetul java.lang.Math.

Exemplul 4.
class Tip extends Thread {
static int total; int i;
Tip(int i) { this.i=i; }
void delay() {
try { sleep( (int) (100 * Math.random()) ); }
catch(InterruptedException e) { }
}
public void run() {
int temp;
for (int j=1; j<=10; j++) {
IO.write(" "+i); delay(); temp=total;
delay(); temp++; delay(); total=temp;
}
}
}
class Grad_Rau {
public static void main(String[] sir) {
Tip[] Ob = new Tip[5];
for (int i=0; i<5; i++) Ob[i] = new Tip(i);
for (int i=0; i<5; i++) Ob[i].start();
try { for (int i=4; i>=0; i--) Ob[i].join(); }
catch (InterruptedException e) { }
IO.writeln(); IO.writeln("Total = " + Tip.total);
}
}

Implementarea în Java a metodei arborelui binar


Este vorba de o metodă generală, cu largă aplicabilitate, din programarea
paralelă1.
Numele ei provine de la faptul că este utilizat un arbore binar complet,
adică un arbore binar cu 2k-1 vârfuri în care:
- vârfurile sunt situate pe nivelurile 0, 1, ..., k-1;
- vârfurile situate pe nivelurile 0, 1, …, k-2 au exact doi descendenţi, iar
cele de pe nivelul k-1 sunt vârfuri terminale (frunze);
- vârfurile de pe un nivel i oarecare sunt numerotate cu 2i..2i+1-1;
descendenţii vârfului i sunt vârfurile 2i şi 2i+1.

Fie vectorul a=(a0,...,an-1) şi o operaţie asociativă ⊗. Se urmăreşte


calculul valorii a0⊗a1⊗...⊗an-1.
Vom presupune că n este o putere a lui 2 (n=2m). În caz contrar vom
completa vectorul la dreapta la o lungime egală cu o putere a lui 2, cu elementul
neutru al operaţiei ⊗:
•0 dacă ⊗ este operatorul de adunare;
•1 dacă ⊗ este operatorul de înmulţire;
• +∞ dacă ⊗ este operatorul de minim;
• -∞ dacă ⊗ este operatorul de maxim;
•0 dacă ⊗ este operatorul de disjuncţie logică;
•1 dacă ⊗ este operatorul de conjuncţie logică.


Pentru consideraţii suplimentare privind programarea paralelă urmăriţi secţiunea 9.3 din
lucrarea: Horia Georgescu, Introducere în universul Java, Editura Tehnică, 2002.
Vom construi un arbore binar complet şi vom ataşa valori vârfurilor sale.
În rădăcină va fi calculată valoarea finală cerută de problemă. Fiecărui nod intern i
se ataşează o valoare parţială şi anume "suma" corespunzătoare (sub)arborelui de
rădăcină i; pe fiecare nivel (adâncime în arbore) calculele se execută în paralel.
Metoda este de tip bottom-up, adică vom parcurge arborele pe niveluri, plecând de
la frunze şi mergând spre rădăcină.

Într-o primă etapă dublăm dimensiunea vectorului şi mutăm elementele


vectorului pe poziţiile an,..,a2n-1 :

for i=0,n-1 in parallel do


an+i ← ai {1}
endfor

În a doua etapă parcurgem arborele de jos în sus, atribuind valori vârfurilor


interne. Valoarea fiecărui vârf se calculează pe baza valorilor descendenţilor săi,
folosind operatorul ⊗.
for k=m-1,0,-1 do
for i=2k,2k+1-1 in parallel {2}
ai ← a2i ⊗ a2i+1
endfor
endfor

Să observăm că valoarea ai ataşată unui vârf i este "suma" valorilor


ataşate frunzelor din subarborele de rădăcină i. Prin urmare rezultatul se obţine în
a1.
Se folosesc n procesoare. Timpul este O(m) = O(log n), deoarece ciclul
for interior necesită timp constant şi se execută de m ori.

Exemplul 5. Ilustrăm cele de mai sus pentru calculul sumei a n numere.


Considerăm vectorul a=(3,7,8,3,9,2,3,1) cu n=8=23 şi m=3.
Operatorul ⊗ va fi în acest caz operatorul de sumare. Figura de mai jos ilustrează
fiecare pas al algoritmului.
Pas 3 a1=17

Pas 2 a2=9 a3=8

Pas 1 a4=5 a5=4 a6=5 a7=3

Iniţializare a8=2 a9=3 a10=1 a11=3 a12=1 a13=4 a14=2 a15=1

Programul care urmează transcrie în limbajul Java metoda arborelui binar.


Clasa "principală" ArbBin conţine câmpurile a (vectorul căruia i se aplică
metoda), n (numărul elementelor vectorului) şi m cu n=2m. Metoda principală
începe cu citirea elementelor vectorului, folosind metodele clasei IO.
Pentru efectuarea calculelor {1}, adică pentru deplasarea elementelor
vectorului pe poziţiile n..2n-1, este folosită clasa Tip1 ce extinde clasa Thread.
Constructorul acesteia furnizează valoarea i, iar valoarea lui n este transmisă
folosind câmpul static al clasei. Metoda run prevede executarea calculului {1}.
Sunt create n obiecte de tipul Tip1, pe baza cărora sunt lansate n fire de executare.
Să observăm că pentru a folosi memorie comună, vectorul a a fost declarat
cu static (variabilă de clasă), astfel încât să poată fi referit din afara clasei,
prefixat cu numele clasei ArbBin în care a fost declarat şi creat. De asemenea
remarcăm utilizarea metodei join pentru aşteptarea terminării firelor de executare
lansate, caracteristică programării paralele.
Pentru efectuarea prelucrării {2} se procedează în mod similar, folosind de
această dată clasa Tip2, ce extinde şi ea clasa Thread.
class Tip1 extends Thread {
static int n; int i;
public Tip1(int i) { this.i = i; }
public void run() { ArbBin.a[n+i] = ArbBin.a[i]; }
}
class Tip2 extends Thread {
int i;
public Tip2(int i) { this.i = i; }
public void run() {
ArbBin.a[i] = ArbBin.a[2*i]+ArbBin.a[2*i+1];
}
}
class ArbBin {
static int[] a = new int[32]; static int n;
public static void main(String[] sir) {
int m=0, i;
IO.write("m= "); m = (int) IO.read();
int n=1; for (i=0; i<m; i++) n *= 2;
IO.write("Numerele : ");
for (i=0; i<n; i++) a[i] = (int) IO.read();
// ------------------------------------------
Tip1[] Ob1 = new Tip1[n]; Tip1.n = n;
for (i=0; i<n; i++) Ob1[i] = new Tip1(i);
for (i=0; i<n; i++) Ob1[i].start();
try { for (i=0; i<n; i++) Ob1[i].join(); }
catch (InterruptedException e) { }
// ------------------------------------------
Tip2[] Ob2 = new Tip2[n];
int p = n/2, u = n;
for (int k=m-1; k>=0; k--) {
for (i=p; i<u; i++) Ob2[i] = new Tip2(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]);
}

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