|
Politica de confidentialitate |
|
• domnisoara hus • legume • istoria unui galban • metanol • recapitulare • profitul • caract • comentariu liric • radiolocatia • praslea cel voinic si merele da aur | |
Programare paralela in Java | ||||||
|
||||||
Cuprins e7g13gf 2. Threading with Java 3. Sincronizare Anexa A : Algoritmi multisegment 1.1 Motivatia Un sistem multiprocesor (SM) este un mecanism care permite unui sistem de a folosi mai mult de un procesor. Sistemul Mutiprocesor Simetric (SMS) este o parte a Calculului Paralel unde toate procesoarele sunt identice. In SMS procesoarele sunt dirijate in asa fel incit sarcinile sunt impartite de catre sistemul de operare iar aplicatiile sunt executate pe mai multe procesoare care impart acelasi spatiu de memorie. SMS garanteaza ca intreg numarul de procesoare deserveste sistemul de operare. Fiecare sub-proces poate fi executat pe orice procesor liber. Astfel se poate realiza echilibrarea incarcarii intre procesoare. Java contine citeva caracteristici care-l fac un limbaj ideal pentru SMS. Este un limbaj orientat obiect foarte simplu si cel mai important, este conceput sa suporte programare multiprocesor. In acest laborator vom prezenta felul in care Java ajuta sa cream aplicatii paralele. Vom face aceasta intr-o maniera incrementala, aratind cum se foloseste fiecare caracteristica a limbajului. 1.1 Thread-uri Ce sunt thread-urile? Thread-ul reprezinta executia liniara a unei singure secvente de instructiuni care ruleaza in interiorul programului nostru. Toti programatorii sunt familiarizati cu scrierea programelor secventiale. Programele secventiale au un punct de start, o secventa de executie si un punct terminal. Cel mai important lucru la programul secvential este acela ca la orice moment o singura instructiune este executata. Thread-ul este similar cu un program secvential in sensul ca thread-ul are si el un punct de start, o secventa de executie si un punct terminal. De asemenea intr-un thread se executa doar o singura instructiune la un moment dat. Si totusi un thread nu este la fel ca un program obisnuit. De ce sa folosim thread-uri? Un singur thread nu ofera nimic nou. Orice program scris pina acum avea cel putin un thread in el. Noutatea apare atunci cind vrem sa folosim mai multe thread-uri, ceea ce inseamna ca aplicatia noastra poate sa faca mai multe lucruri in acelasi timp. Fiecare thread poate sa faca altceva in acelasi timp: unul sa incarce o pagina Web in timp ce altul animeaza o icoana sau toate pot colabora la acelasi job (generarea unei imagini 3D). Cind se foloseste corespunzator multithreading-ul cresc mult performantele appletului sau aplicatiei unde este folosit. Multithreading-ul poate simplifica fazele de proiectare si planificare a tuturor aplicatiilor greu de realizat intr-un program secvential. Astfel poate ajuta programatorul pentru a crea programe mai performante (caracteristica programmer friendly). Un exemplu bun de thread-uri este un procesor de text care poate sa tipareasca o pagina (paginare, incadrare si trimitere catre imprimanta) in background. Se poate continua editarea in timp ce pagina este trimisa catre imprimanta. Va imaginati cit de greu ar fi de scris un program secvential intretesut care sa realizeze acest lucru? Concurenta thread-urilor. Fara a intra intr-o discutie pe teme hardware, este bine de spus ca procesoarele calculatoarelor pot executa doar o instructiune la un moment dat. De ce spunem ca thread-uri diferite se executa in acelasi timp? Contextul thread-urilor si memoria distribuita Thread-urile ruleaza in contextul unui program, folosind resursele acestuia. 1.3 Java Java este un limbaj de programare orientat obiect, simplu, robust, sigur, cu arhitectura neutra, ce permite multithreading, dinamic dezvoltat de firma Sun Principalele carateristici Java Java a fost creat nu numai pentru ca este portabil pe orice platforma de sistem
de operare ci de asemenea prezinta caracteristica ca este compilat in forma binara (binary
form compiled). Spre deosebire de un cod executat pe o masina care pe alte platforme
este imposibil de executat, Java este compilat intr-un limbaj masina intermediar Java contra C++ Java a fost creat original deoarece C++ era inadecvat pentru task-uri efective.
Applete contra aplicatii Applet-urile sunt aplicatii speciale Java care pot fi incarcate si executate
de catre browser-ele Web. Apleturile pot fi integrate in paginile Web si sunt automat incarcate cind browser-ul afiseaza aceste pagini. Spre deosebire de
aplicatii, apletele nu pot fi executate in afara browser-ului Web. Din moment
ce apletele sunt incarcate de catre browser-ele Web de la un server Web si se
executa pe masina locala a utilizatorului, ele au unele restrictii. In era virusilor de calculatoare este crucial ca o aplicatie sa nu fie in stare sa
acceseze fisierele protejate de pe masina utilizator sau sa stearga intregul
continut al HDD-ului. Avind in vedere aceasta problema, proiectantii limbajului
Java pe scurt Limbajul Java prezinta caracteristicile: 1 Tipuri bine definite de obiecte. Rezumind, putem spune simplu ca mediu Java care ruleaza pe un Sistem Multiprocesor Simetric creaza cea mai puternica combinatie soft-hard. 2. Programare multithreading in Java Dupa ce ne-am familiarizat cu conceptul de thread este timpul sa vedem cum este suportat multithreading-ul de catre Java. Thread-urile Java sunt implementate de clasa Thread care este parte din package-ul java.lang. Clasa Thread implementeaza thread-urile independente sistem. Actuala implementare a thread-urilor este realizata de catre sistemul de operare si clasa Thread permite interfatarea cu toate sistemele. 2.1 Crearea unui Thread, crearea unui thread de executie In Java, fiecare thread este incapsulat intr-o clasa si ruleaza prin intermediul
unor metode specifice in instanta unei clase. Aceasta instanta nu este o instanta a clasei Thread ci este o instanta a unei clase derivata din clasa
1 sa fie ori derivata din clasa Thread, fiind o clasa care incapsuleaza facilitatile
thread-ului ori class MyThread extends Thread A public void run() A S S ________________________________________________________________________ Acest exemplu de clasa derivata din clasa Thread suprascrie una din metodele
sale - metoda run(). Metoda run() este cea mai importanta deoarece contine codul
pe care thread-ul il va executa. Pentru majoritatea thread-urilor aceasta metoda
contine o bucla infinita. Pentru a lansa in executie metoda run() mai intii
trebuie creata o instanta a acestei clase, apoi sa se apeleza metoda start()
a acestei clase. Metoda start() face thread-ul activ si invoca metoda run(). class MyTest A public static void main(String Argsai) A new MyThread().start(); Aceasta secventa de cod combinata cu clasa MyTread va afisa mesajul "Hello world!" pe ecran. Ce se intimpla de fapt aici : metoda main va starta thread-ul si se va termina. Thread-ul nu se va termina cind se va termina metoda main. El va executa metoda run() pina aceasta se termina. Metoda run() va afisa efectiv mesajul "Hello world!" pe ecran si apoi va iesi. Cind si metoda main() si metoda run() se vor fi terminat, se poate reda controlul sistemului. O alta metoda de a crea thread-uri este aceea ca o clasa sa implementeze interfata Runnable asa cum este aratat in exemplul urmator: class MyThread implements Runnable A public void run() A O clasa care implementeaza interfata Runnable poate fi derivata din orice clasa. class MyTest A public static void main(String Argsai) A new Thread(new MyThread()).start(); Observam ca de aceasta data folosim un alt constructor al clasei Thread pentru
a instantia un thread. Acest constructor necesita un parametru care este o referinta
la o instanta a unei clase care implementeaza interfata Runnable. Mai exista
si alti constructori in clasa Thread, cum ar fi : Thread(ThreadGroup, Runnable,
String). Thread-uri daemon Multe sisteme prezinta thread-uri cu scopul de a facilita diverse servicii
(servicii de I/O, ascultare pe socket, etc). Aceste thread-uri sunt majoritatea in stare
idle si numai cind primesc un mesaj ele incep sa-si execute task-ul specific.
2.2 Executia paralela a thread-urilor Cu toate ca exemplele de pina acum nu au fost in masura sa puna in evidenta vreun nivel de paralelism, ele de fapt au facut acest lucru. Odata ce s-a terminat executia instructiunii Thread.start(), instanta clasei MyThread isi incepe executia. Nu se poate spune cu siguranta ca mesajul de pe ecran a fost tiparit dupa terminarea metodei main(), tiparirea pe ecran putind avea loc si inaintea terminarii metodei main(). Totul depinde de felul in care au fost alocati timpii procesor la thread-uri si de faptul daca exista unul sau mai multe procesoare. Pentru a demonstra acest principiu, sa consideram urmatorul program si rezultatul lui: class PrintThread implements Runnable A class Test A public static void main (String Argsai) A new Thread(new PrintThread("A")).start(); new Thread(new PrintThread("B")).start(); Iesirea programului de mai sus poate arata cam cum urmeaza (rulat in Windows NT si pe o masina multiprocesor va fi sigur asa): AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB Cu un numar egal de "A" si de "B". Acest exemplu este menit sa demonstreze faptul ca aceste doua thread-uri lucreaza in paralel. Multithreading preemptiv contra multithreading non-preemptiv Multithreading preemptiv inseamna faptul ca un thread poate fi preemptat (suspendat) de catre alt thread in timp ce se executa. Nu toate sistemele care suporta multithreading prezinta mecanism preemptiv. Iesirea aceluiasi program pe un sistem SPARC/Solaris 2.5 ar arata in felul urmator: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Acest lucru este datorat faptului ca pe Solaris (si nu numai) thread-urile nu sunt preemptive. Un thread trebuie sa aiba "o comportare echitabila" si sa renunte la timpul sau procesor in asa fel incit sa permita si altor thread-uri sa se executa. Renuntarea la propriul timp CPU in mod voluntar se realizeaza prin invocarea metodei proprii yield(). In continuare iata o versiune revizuita a clasei PrintThread care elibereaza CPU-ul dupa fiecare litera afisata: class WellBehavedPrintThread implements Runnable A Instructiunea Thread.currentThread().yield() utilizeaza o metoda publica a clasei Thread pentru a capata handler-ul catre thread-ul curent si apoi ii spune sa elibereze procesorul. Iesirea acestui exemplu este: ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB 2.3 Starile thread-urilor Creind o instanta a unui thread acesta nu este lansat. Aceasta sarcina de a lansa thread-ul in executie este realizata de metoda start(). Un thread se poate gasi in stari diferite in functie de evenimentele trecute. In tabelul urmator se pot observa starile in care se poate gasi un thread: Stare Descriere Instanta nou creata 1. Metoda run() nu este in executie, timpul procesor nu este inca alocat. Pentru a starta un thread trebuie apelata functia start(). In aceasta stare se poate apela de asemenea metoda stop(), care va distruge thread-ul. 2. Thread in executie 3. Thread gata de executie (runnable) 4. Thread nepregatit pentru executie (non runable thread) 2. Prioritatile thread-urilor Fiecarui thread ii este asignata o prioritate cuprinsa intre MIN_PRIORITY class LowPriority extends Thread A public void run()A setPriority(Thread.MIN_PRIORITY); for(;;)A class Spawner A public static void main( String argsai ) A Starting threads... Sa analizam putin iesirea programului: 1. Prima linie este afisata de thread-ul principal. Thread-ul principal lanseaza thread-ul de prioritate minima si thread-ul de prioritate maxima. Sistemul poate genera o exceptie de tipul IllegalThreadStateException cind
este apelata o metoda a unui thread cind starea acestuia nu permite acest lucru.
De exemplu, exceptia, IllegalThreadStateException este generata cind se face apelul
metodei suspend() unui thread care nu este runnable. Si un ultim amanunt despre
starile thread-urilor: clasa Thread cuprinde o metoda numita isAlive() care
returneaza true daca thread-ul a fost startat dar nu stopat si false cind thread-ul este in starea new Thread sau Dead. Nu se poate face distinctie intre
un thread in stare new Thread si un thread in stare Dead si nici intre unul
Grupuri de thread-uri Fiecare thread apartine unui grup de thread-uri. Un grup de thread-uri este
o multime de thread-uri (si posibil grupuri de thread-uri) impreuna cu un mecanism de realizare a operatiilor asupra tuturor membrilor multimii. Grupul
implicit de thread-uri este implicit numit main, si fiecare group de thread-uri
nou creat apartine acestui grup, mai putin acelea specificate in constructorul
sau. Pentru a afla pentru un thread la ce grup de thread-uri apartine se poate
folosi metoda getThreadGroup(). Pentru a crea propriul nostru grup de thread-uri,
trebuie mai intii sa creem un obiect ThreadGroup. Se poate folosi unul din acesti constructori : ThreadGroup(String) - creaza un nou ThreadGroup cu numele
specificat. ThreadGroup(threadGroup, String) - creaza un nou ThreadGroup cu
numele specificat si apartinind la un anumit grup de thread-uri. Dupa cum arata si
al doilea constructor, un grup de thread-uri poate fi creat in interiorul altui
grup de thread-uri. Cel mai nou grup de thread-uri creat devine membru la cel
mai vechi realizindu-se astfel o ierarhie de grupuri. Pentru a crea un thread
in interiorul unui grup de thread-uri, altul decit grupul main trebuie doar
mentionat numele grupului atunci cind se apeleaza constructorul thread-ului.
3.0 Sincronizare Cind se utilizeaza mai multe thread-uri avem nevoie de o sincronizare a activitatilor lor. Exista cazuri in care dorim sa prevenim accesul concurent la structurile de date ale programului care reprezinta secvente comune acestor thread-uri. Limbajul Java ne ajuta in acest sens cu ajutorul unui mecanism de sincronizare si excludere mutuala (permite numai unui singur thread sa opereze asupra unei sectiuni critice). Sincronizarea dintre thread-uri in Java se realizeaza folosind metodele notify() si wait(). Ecluderea mutuala se realizeaza prin folosirea monitoarelor. Motivarea sincronizarii Sa consideram urmatoarea clasa al carei obiectiv este de a stoca date: class MyData A private int Data; public void store(int Data) A this.Data=Data; Acum sa presupunem ca avem doua thread-uri: unul care incearca sa depoziteze o valoare si unul care incearca sa scoata o valoare. In continuare se prezinta codul care creaza cele doua thread-uri. Pentru a simula procesarea in timp real, vom cere thread-urilor sa "adoarma" dupa fiecare extragere de data: class Main A// Clasa utilizata pentru a pune lucrurile in miscare public static void main(String argvai) A class Producer implements Runnable A//Thread-ul - Producator class Consumer implements Runnable A //Thread-ul - Consumator Acest program consta din doua thread-uri: Consumer (consumator) si Producer
Producer: 0 Dupa cum se observa, numerele 2, 4, 8 si 9 sunt produse dar nu sunt niciodata
consumate. Pe de alta parte, numerele 1 si 7 sunt produse o singura data, dar
consumate de cite doua ori. Acest lucru este datorat ordinei gresite de executie.
O prima solutie. Pentru a rezolva aceasta problema se pot folosi variabile binare pentru a controla accesul la data. Flag-ul Ready va semnifica faptul ca noua data a fost produsa si este gata de consum si flag-ul Taken va semnifica faptul ca aceasta data a fost consumata si este gata de suprascriere. class MyData A private int Data; private boolean Ready; private boolean Taken; public MyData() A Utilizind codul de mai sus vom obtine rezultatul asteptat : fiecare numar este
consumat o singura data si toate numerele sunt consumate. Oricum aceasta solutie are
un dezavantaj major : metodele store() si load() folosesc bucle, thread-urile testeaza in mod constant flag-urile pentru a vedea daca valorile lor s-au schimbat. Utilizarea buclelor de test ar putea determina ca acest program sa
nu functioneze pe platforme nepreemptive deoarece thread-urile nu elibereaza procesorul si astfel thread-ul care trebuie sa schimbe o valoare, poate sa nu
fie planificat pentru executie (aceasta problema ar putea fi rezolvata folosind
apelurile metodei yield() in aceste bucle). O alta problema in aceste conditii
poate aparea atunci cind se foloseste acelasi cod dar cu mai multi consumatori.
Consumer1 Consumer2 Producer Dupa cum se poate observa cei doi consumatori consuma aceeasi valoare. Aceasta se poate intimpla usor cind thread-urile impart acelasi procesor prin preemptare, sau procesoare multiple care ruleaza thread-urile in acelasi timp. O solutie la aceasta ultima problema este utilizarea mecanismului de sincronizare din Java, mecanism care are la baza monitorul. 3.1 Monitoare Ce este un monitor? Un monitor (pentru prima data introdus de Hoare in 1974) este asociat intodeauna
cu o data specifica si o functie care controleaza accesul la aceasta data. Cind
un thread retine un monitor pentru a accesa o data, celelalte thread-uri sunt
blocate si nu pot avea acces la acea data. Un thread poate prelua un monitor
numai atunci cind celelalte thread-uri nu l-au preluat si il poate elibera cind
doreste. Poate exista un monitor pentru fiecare instanta a unei clase care are
o metoda declarata ca synchronized. Declararea unei metode synchronized indica
faptul ca numai acest thread care contine monitorul poate executa aceasta metoda.
Sincronizarea utilizand monitoare In urmatorul exemplu al clasei MyDatase foloseste metode de tip synchronized: class MyData A private int Data; private boolean Ready; private boolean Taken; public MyData() A public synchronized void store(int Data) A public synchronized int load() A Metodele "sincronizate" elimina nevoia de a stoca variabile de tip
Data in interiorul metodei load(), astfel ca metodele load() si store() nu vor putea
sa se execute in acelasi timp in thread-uri diferite. O problema apare, insa, cind
un thread este "surprins" intr-o bucla de test si detine inca monitorul.
class MyData A private int Data; private boolean Ready; private boolean Taken; public MyData() A public void store(int Data) A De notat faptul ca atunci cind folosim cuvintul cheie synchronized pe un segment de cod, trebuie sa declaram obiectul insusi ca parametru al lui synchronized. Si totusi o problema ramine: bucla de test. Aceasta practica de a folosi bucle de test este considerata a fi gresita pentru thread-uri: consumatoare mare de timp procesor intr-o implementare preemptiva care, dupa cum s-a vazut, cauzeaza interblocarea intr-o implementare nonpreemptiva. Asteptarea de evenimente Exista totusi o cale de a evita buclele de test si in acelasi timp de a elimina
necesitatea folosirii unuia din flag-uri utilizate pentru sincronizare. Solutia
este de a folosi metodele wait() si notify() care sunt membre ale clasei Object
din care este derivata orice clasa (aceste metode exista pentru orice obiect
in Java). Metodele wait() si notify() sunt utilizate pentru determinarea asteptarii unui eveniment si respectiv trimiterea lor la un thread. Acest mecanism functioneaza dupa cum urmeaza: metoda wait() face ca thread-ul sa elibereze monitorul si il comuta pe acesta din starea runnable in starea non-runnable. Thread-ul va astepta in aceasta stare din urma pina cind este
class MyData A private int Data; private boolean Ready; public MyData() A wait(); S catch (InterruptedException e) A S this.Data=Data; Ready=true; notify(); S public synchronized int load() A while (!Ready) try A wait(); S catch (InterruptedException e) A S Ready=false; notify(); return this.Data; S S In acest moment avem rezultatele dorite: fara a avea bucle wait si fara probleme de sincronizare. De observat faptul ca metodele wait() si notify() pot fi apelate numai din metode synchronized. A fi reentrant inseamna faptul ca codul clasei este protejat impotriva accesului multiplu. Toate clasele construite in Java sunt reentrante ceea ce inseamna ca ele pot fi folosite in programarea multithreading fara probleme. Din moment ce Java este pentru reutilizarea obiectelor si din moment ce nu stim cind sa utilizam o clasa din nou, este necesar sa o proiectam reentranta inca de la inceput. Ultima implementare a clasei MyData este o implementare reentranta. Sa vedem ce se intimpla cind mai multe thread-uri incearca sa acceseze aceeasi metoda simultan: a. Mai multi consumatori - un singur producator. Consideram urmatoarea problema: producatorul nu a produs inca nimic. Primul
consumator preia monitorul dar va intra in wait() si-l va elibera permitind
ca al doilea consumator sa urmeze aceeasi cale. Vom avea eventual toti consumatorii
in stare wait() si monitorul eliberat. In acest moment producatorul intra in
actiune: produce si apeleaza notify(). Aceasta cauzeaza faptul ca unul din thread-urile in wait() incearca sa preia monitorul. Din moment ce producatorul
tocmai a eliberat monitorul iesind din segmentul de cod synchronized aceasta
nu va cauza nici o problema. Consumatorul va "consuma" data si va apela
notify(). b. Mai multi producatori - un singur consumator Acum situatia este viceversa: acum sunt mai multi producatori si un singur
consumator. Acum producatorii vor astepta (wait()) in timp ce consumatorul va
3.2 Bariere In aplicatiile multithreading este necesar ca anumite thread-uri sa se sincronizeze la un anumit punct. Un exemplu este calculul paralel in faza, in care toate thread-urile trebuie sa-si termine faza de executie inainte de a trece toate odata la faza urmatoare. O bariera este un mecanism folosit pentru a sincroniza mai multe thread-uri. Un thread care intilneste o bariera intra automat in wait(). Cind ultimul thread "ajunge" la bariera, semnaleaza (notify()) si celorlalte thread-uri care sunt in asteptare rezultind o "trecere" in grup a barierei. Iata un exemplu in acest sens: import java.util.*; class Barrier A // Clasa Barrier sincronizeaza toti Ajungind la bariera toate thread-urile, mai putin ultimul, asteapta in interiorul
metodei synchronized ca ultimul thread ajuns sa le elibereze. Odata ce s-au
3.3 Asteptarea terminarii unui thread Uneori este necesar a se astepta terminarea unui thread. De exemplu tread-ul
principal (main) poate crea un al doilea thread pentru a executa ceva in interiorul
lui. Java permite monitorizarea starii unui thread, altul decit cel in care
se face aceasta operatie si suspendarea executiei pina ce acesta se termina. O
metoda care poate fi folosita este metoda isAlive() care returneaza true daca
thread-ul invocat nu este dead. Pentru asteptarea terminarii unui thread (fara
a utiliza bucle) putem utiliza metoda join(). Aceasta metoda face ca un thread
sa astepte pina cind un alt thread isi termina executia urmind sa-si reia executia in momentul in care thread-ul asteptat s-a terminat. Un thread poate
fi intrerupt prin apelarea metodei interrupt(), caz in care metoda join() va
Class MainThread extends Thread A public void run() A 3.4 Alte metode de sincronizare In regula, veti spune, Java are aceasta sincronizare cu monitoare dar eu vreau sa am vechile mele semafoare. Nici o problema. Utilizind monitoare se poate implementa orice obiect sincronizat dorit inclusiv semafoare. Iata in continuare un exemplu de semafor in Java: class Semaphore A protected int value; 3.5 Flaminzirea (Starvation) Termenul de flaminzire caracterizeaza situatiile in care un thread este privat de resurse (accesul la un monitor). Spre deosebire de interblocare, in situatia de flaminzire calculele pot continua in sistem, doar ca thread-ul flaminzit nu mai poate continua. Flaminzirea se poate produce atunci cind un thread de prioritate mai mare isi incepe executia si nu mai elibereaza procesorul. De altfel toate thread-urile de prioritate mai mica sunt flaminzite. 3.6 Interblocarea (Deadlock) Interblocarea se produce cind unul sau mai multe thread-uri asteapta schimbarea unei conditii in timp ce acea conditie este exclus sa se schimbe deoarece toate thread-urile care ar putea face acest lucru sunt in asteptare. Am vazut cum poate aparea un interblocaj atunci cind se folosesc bucle de test in interiorul unui monitor, asteptind ca un alt thread sa schimbe o conditie dar fara ai da posibilitatea de a obtine monitorul. Anexa A: Algoritmul multisectiune In aceasta anexa va prezentam un exemplu practic de utilizare a thread-urilor.
import java.lang.Math; // pentru functia abs() class Bis A // Aceasta clasa detine functia main() synchronized void mynotify() A |
||||||
|
||||||
|
||||||
Copyright© 2005 - 2024 | Trimite document | Harta site | Adauga in favorite |
|