Home - Rasfoiesc.com
Educatie Sanatate Inginerie Business Familie Hobby Legal
Doar rabdarea si perseverenta in invatare aduce rezultate bune.stiinta, numere naturale, teoreme, multimi, calcule, ecuatii, sisteme




Biologie Chimie Didactica Fizica Geografie Informatica
Istorie Literatura Matematica Psihologie

Java


Index » educatie » » informatica » Java
» Programare multi-thread in Java


Programare multi-thread in Java


Programare multi-thread in Java

1. Introducere

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.

Spre deosebire de programe, thread-ul nu poate exista de unul singur. Thread-urile coexista in interiorul unui program. Deci putem avea mai multe thread-uri care se executa simultan.

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?

Utilizand thread-uri putem avea un thread care tipareste si unul care permite utilizatorului sa continue editarea. Alt exemplu este acela a unui browser Web care permite defilarea unui text al unei pagini pe care tocmai ati incarcat-o in timp ce browser-ul se ocupa cu aducerea imaginilor. Ce neplacut ar fi sa astepti incarcarea unei intregi pagini pe care o gasesti in final neinteresanta.

Thread-urile pot face calculul mai rapid. Prin segmentarea unui task in subtask-uri si apoi avind cite un thread pentru fiecare subtask se poate creste mult viteza de executie. Aceasta este general valabil pentru un SMS. Cind se executa doua thread-uri, ele vor fi executate simultan fiecare pe cite un procesor si astfel se realizeaza o crestere semnificativa a vitezei. Avantaje similare se obtin cind se utilizeaza Java pe alte platforme de calcul paralel si distribuit cum ar fi Sistemele cu Memorie Distribuita (SMD).

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?

Spunind simultan nu inseamna numai lucruri in medii diferite. Pe o masina multiprocesor, thread-urile pot exista pe procesoare diferite in acelasi timp fizic, aceasta mentinindu-se valabil chiar daca procesoarele sunt pe calculatoare diferite conectate intr-o retea. Dar si pe o masina cu un singur procesor, thread-urile pot imparti acelasi procesor, rulind intr-o maniera intretesuta, competitia pentru timpii CPU creind iluzia ca ele se executa simultan. Aceasta iluzie pare reala atunci cind, de exemplu, 30 de imagini distincte pe secunda captate de ochiul uman sunt percepute intr-un flux continuu de imagine. Aceasta comutare intre thread-uri are si ea un pret. Consuma timp CPU pentru ca acesta sa inghete starea unui thread si sa dezghete starea unui alt thread (schimbare de context). Daca thread-urile concurente sunt executate pe acelasi procesor si toate executa calcule atunci timpul total de executie nu va lua mai mult decit timpul de executie al unui program secvential care realizeaza acelasi lucru.

Din moment ce intr-un sistem monoprocesor thread-urile concura la timpul procesor cum este posibil cresterea vitezei sistemului? Aceasta se realizeaza prin intreteserea diferitelor faze ale diferitelor thread-uri. Multe task-uri pot fi segmentate logic in tipuri de faze: faza de calcul si faza I/O. Faza de calcul necesita atentia maxima din partea CPU-ului prin utilizarea diferitelor metode de calcul. Faza de I/O (intrare/iesire) necesita atentie maxima din partea perifericelor (imprimante, hard discuri, placi de retea, etc) si in aceste situatii procesorul este in general liber, asteptind ca perifericul sa-si termine sarcina. Cresterea vitezei este obtinuta prin intreteserea fazelor. In timp ce un thread se afla intr-o faza de I/O asteptind ca o secventa de date sa fie incarcata de pe hard disk, un thread cu o faza de calcul poate ocupa procesorul si cind ajunge la o faza I/O, celalalt thread (care tocmai a terminat faza I/O proprie) poate incepe sa utilizeze CPU-ul.

Contextul thread-urilor si memoria distribuita

Thread-urile ruleaza in contextul unui program, folosind resursele acestuia. Fiecare thread are propriile variabile si puncte de executie, dar variabilele globale sunt impartite de toate thread-urile. Deoarece ele impart acelasi spatiu (variabilele globale si alte resurse) toate acestea pot accesa la un moment dat aceeasi data. Este important de observat ca memoria comuna prezinta o degradare a performantelor cind lucreaza cu un cluster de computere conectate intr-o retea, asemenea cazului unui sistem distribuit de tip clustere de PC-uri. Obiectele care sunt comune trebuie sa fie transmise pe retea de atitea ori de cit este nevoie. In asemenea mediu distribuit, reteaua poate fi privita ca o resursa cu efect secvential (git de sticla). Pentru un programator intr-un sistem cu memorie distribuita, impartirea memoriei este un lucru transparent, relativ la nivelul cel mai adinc al sistemului de operare. Oricum, in asemenea sisteme, trebuie avut o atentie sporita pentru a minimiza utilizarea concurenta a variabilelor comune de mai multe thread-uri, deoarece aceasta situatie ar putea degenera intr-o sufocare a aplicatiei.

1.2 Java

Java este un limbaj de programare orientat obiect, simplu, robust, sigur, cu arhitectura neutra, ce permite multithreading, dinamic dezvoltat de firma Sun Microsystems. Bazat pe sintaxa C++, Java este un limbaj simplu de invatat de cei care au cunostinte de baza din C si C++.

Principalele carateristici Java

Java a fost creat nu numai pentru ca este portabil pe orice platforma dotata  cu un sistem de operare ci si pentru faptul 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 (o secventa de instructiuni numita byte-code) care este interpretat din mers de interpretorul Java. Compilarea Just In Time (JIT) permite byte-code-ului de a rula la fel de repede ca un cod masina nativ.

Interpretorul Java este o colectie de thread-uri care realizeaza task-uri diferite pentru a putea suporta executia unui program Java. Intre thread-urile interpretorului sunt cele a caror sarcina este de a executa instructiuni byte-code, unul care gestioneaza iesirea grafica si colectorul de deseuri (garbage collector). Fiind un limbaj orientat obiect complet, Java suporta atit clase cit si interfete. O interfata este o declaratie a tipului unei clase, impreuna cu definirea unui set de metode (functii) pe care clasa respectiva le implementeaza. Interfetele sunt clase virtuale C++, care contin metode virtuale pure. Java suporta mostenirea simpla (numai o clasa de baza pentru o clasa derivata), dar adauga calitatea de polimorfism prin implementarea interfetelor multiple.

Java capata robustete prin eliminarea necesitatii alocarii de memorie. Pentru a facilita aceasta cit si alte caracteristici, Java foloseste instante de obiecte in loc de pointeri. Similar in folosire, instantele de obiecte pot fi create numai de sistem si apoi trimise catre aplicatia noastra. Cind ultimul obiect este eliberat nu exista nici o modalitate de a recapata controlul asupra lui si obiectul devine candidat pentru colector. Executind un thread care este colector in background este asigurat faptul ca memoria va fi eliberata oricind este nevoie. Din moment ce aplicatiile multiproces, procesoarele, impart datele de la resursele comune, se ridica atunci problema excluderii reciproce. Java, de asemenea prezinta tot ce este necesar pentru task-uri, prezentind facilitatea de sincronizare. In plus, Java contine un mecanism de manipulare a grupului de thread-uri. Kit-urile de dezvoltare de Java actuale sunt disponibile pentru diferite platforme: Unix, Windows si alte sisteme de operare.

Applete contra aplicatii

Applet-urile sunt aplicatii speciale Java care pot fi incarcate si executate de catre browser-ele Web. Applet-urile pot fi integrate in paginile Web si sunt automat incarcate cind browser-ul afiseaza aceste pagini. Spre deosebire de aplicatii, appletii nu pot fi executati in afara browser-ului Web. Din moment ce appletii sunt incarcati 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 au limitat accesul in applet, permitind conectare numai in locul de unde au fost solicitati. Nici o restrictie nu este pusa applet-ului in privinta crearii unui thread. Incarcarea unui applet poate genera crearea unui numar mare de thread-uri, facind ca procesorul sa se supraincarce. Aceasta nu este o problema majora din moment ce nu dauneaza securitatii sistemului.

Limbajul Java prezinta caracteristicile:

1 Tipuri bine definite de obiecte.

2 Nu contine pointeri aritmetici, nu necesita alocare si dealocare de memorie si nu foloseste pointeri cast 'falsi'.

3 Java este un limbaj orientat obiect. Astfel incapsularea, polimorfismul sunt usor de atins.

4 Nu exista contructii nesigure.

5 Este un limbaj mic si are o infatisare familiara, astfel el devine fluent.

6 Concurentialitatea: Java este un limbaj multithreading. Modelul include thread-uri si posibilitati de sincronizare (bazat pe conceptul de monitor).

7 Multitaskingul este realizat in pagini de memorie comune.

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 Thread sau a unei clase care implementeaza interfata Runnable (Runnable Interface).

Clasele care instantiaza astfel de obiecte sunt numite 'runnable classes' si obiectele sunt numite 'runnable objects'. Executia unui thread incepe cu apelarea unei methode din clasa Runnable sau dintr-o subclasa a acesteia. Aceasta metoda se executa atita timp cit ea exista si apoi thread-ul moare (aceasta implica faptul ca o metoda care executa o bucla infinita ca thread-ul asociat nu moare niciodata).

Un thread poate sa fie terminat ca urmare a unui eveniment extern cum vom vedea mai departe. Pentru ca un thread sa poata sa execute o metoda a clasei trebuie sa indeplineasca una din conditiile:

1 sa fie ori derivata din clasa Thread, fiind o clasa care incapsuleaza facilitatile thread-ului ori

2 sa implementeze interfata Runnable, o interfata in care orice clasa trebuie sa se comporte ca un thread separat. Clasa Thread de asemenea implementeaza interfata Runnable.

class MyThread extends Thread

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 se apeleza metoda start() a acestei clase. Metoda start() face thread-ul activ si invoca metoda run().

class MyTest

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

O clasa care implementeaza interfata Runnable poate fi derivata din orice clasa.

class MyTest

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). Parametrul ThreadGroup asigneaza thread-ul la un grup de thread-uri, o multime de thread-uri care permite ca toate thread-urile sa fie tratate ca un intreg.

Parametrul Runnable este sursa unei noi metode run() a unui thread (un handler catre un obiect Runnable). Parametrul de tip String suporta un nume pentru acest nou thread. Acest nume poate fi folosit impreuna cu metoda getName() a clasei Thread pentru o referire de tip mnemonic a unui thread. Acesti parametri sunt optionali (de altfel exista 7 tipuri diferite de constructori pentru clasa Thread).

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.  Aceste thread-uri sunt cunoscute ca 'Daemon Threads'. Orice thread poate deveni daemon prin apelarea metodei setDaemon() proprii cu valoarea true. Se poate verifica starea unui thread utilizand metoda isDaemon().

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

public void run()

class Test

Iesirea programului de mai sus poate arata cam cum urmeaza (rulat in Windows NT si pe o masina multiprocesor va fi sigur asa):

A A A A A A A A AA B B B B B B B B B B B

B B A A A A A A A A A A AA B B B B B B B B

B B B BB A A A A A A A A A A AAA B B B B B B

B B B B B A A A A A A A A A B B B B B B

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:

A A A A A A A A A A A A A A A A A A A A A A A

A A A A A A A A A A A A A A A A A A A A A A A

A A A A A A A A A A A A A A A A A A A A A A A

A A A A A A A A A A A A A A A A A AAA

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

public void run()

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:

A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB

A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB

A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB A BAB

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 petrecute.

1. Thread nou creat - 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 - Thread-ul a fost startat cu metoda start(). Este thread-ul pe care procesorul il executa in acest moment.

3. Thread gata de executie (runnable) - Thread-ul a fost startat dar nu este in executie pe procesor in acest moment.

Motivul ar fi acela ca thread-ul a renuntat la procesor apelind metoda yeld(), sau din cauza unui mecanism de programare a timpului procesor care a decis sa distribuie timpul procesor al acestui thread altui thread. Thread-ul va putea trece in executie cind mecanismul de programare va decide acest lucru. Atita timp cit exista un alt thread cu o prioritate mai mare, thread-ul nu va trece in executie.

4. Thread nepregatit pentru executie (non runnable thread) - Thread-ul nu poate fi executat din anumite motive. Ar putea sa astepte o operatie de tip I/O sa se termine. A trebuit sa apeleze metoda wait(), sleep() sau suspend().

Prioritatile thread-urilor

Fiecarui thread ii este asignata o prioritate cuprinsa intre MIN_PRIORITY (egala cu 1) si MAX_PRIORITY (egala cu 10). Un thread mosteneste prioritatea de la thread-ul care l-a creat, dar aceasta prioritate se poate schimba apelind metoda setPriority() si aceasta prioritate se poate afla apelind getPriority().

Algoritmul de planificare intodeauna va lansa thread-ul cu prioritatea cea mai mare sa se execute. Daca exista mai multe thread-uri cu aceeasi prioritate maxima atunci procesorul le va executa intr-o maniera round-robin. Astfel un thread cu prioritate mica se poate executa numai atunci cind toate thread-urile de prioritate mai mare sun in starea non-runnable(). Prioritatea thread-ului main

este NORM_PRIORITY( egala cu 5). Iata un exemplu de utilizare a prioritatilor:

class LowPriority extends Thread

}

class HighPriority extends Thread catch (InterruptedException e)

}

}

class Spawner

Iesirea acestui program va arata ca mai jos:

Starting threads

The LOW priority thread is running

The LOW priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

MAIN is done

The LOW priority thread is running

The LOW priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

The HIGH priority thread is running

The LOW priority thread is running

The LOW priority thread is running

The HIGH priority thread is running

^C

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.

2. Threadul de prioritate minima incepe sa se execute, acesta este facut imediat non-runnable de catre un thread de prioritate mai mare.

3. Thread-ul de prioritate mai mare incepe sa se execute, isi seteaza prioritatea maxima si astfel isi incepe executia. Dupa afisarea a 5 linii, thread-ul este 'adormit' si thread-ul main devine cel mai prioritar.

4. Thread-ul main afiseaza un mesaj dupa care se termina. In acest moment singurul thread gata de executie (runnable) este thread-ul de prioritate mica.

5. De aici incolo iesirea programului va arata la fel.

6. Thread-ul de prioritate mica va afisa mesaje proprii pina cind thread-ul de prioritate mai mare se va trezi.

7. Thread-ul de prioritate mare incepe sa se execute, afiseaza 5 linii si 'adoarme' din nou.

8. Thread-ul de prioritate minima poate rula din nou

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 Runnable si unul not Runnable.

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.

In momentul in care avem mai multe thread-uri organizate intr-un grup de thread-uri putem apela la operatii comune pentru toti membrii acestui grup.

Aceste operatii sunt in principal stop(), supend() and resume() care au aceeasi semnificatie ca atunci cind se foloseste un singur thread. Pe langa aceste operatii mai exista si alte operatii specifice grupului de thread-uri (vezi 'Java Application Programing Interface').

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(). Excluderea mutuala se realizeaza prin folosirea monitoarelor.

Motivarea sincronizarii

Sa consideram urmatoarea clasa al carei obiectiv este de a stoca date:

class MyData

public int load()

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

class Producer implements Runnable

public void run() catch (InterruptedException e)

}

}

class Consumer implements Runnable

public void run() catch (InterruptedException e)

}

}

Acest program consta din doua thread-uri: Consumer (consumator) si Producer (producator). Producatorul 'produce' si stocheaza datele in structura comuna celor doua thread-uri utilizind metoda store(). Consumatorul extrage acele date din structura MyData comuna. La prima vedere codul pare a fi in regula, dar acesta nu este din moment ce iesirea programului arata astfel:

Producer: 0

Consumer: 0

Producer: 1

Consumer: 1

Consumer: 1

Producer: 2

Producer: 3

Consumer: 3

Producer: 4

Producer: 5

Consumer: 5

Producer: 6

Consumer: 6

Producer: 7

Consumer: 7

Consumer: 7

Producer: 8

Producer: 9

Producer: 10

Consumer: 10

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. In primul rind ca producatorul nu are cum sa stie daca consumatorul a consumat data si in consecinta suprascrie noua data. In al doilea rind, consumatorul nu are cum sa stie daca citeste noua valoare sau pe cea veche. Este deci nevoie de o comunicare intre cele doua thread-uri.

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

public void store(int Data)

public int load()

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 apare atunci cind se foloseste acelasi cod dar cu mai multi consumatori.

Rulind mai mult de un thread de tip Consumer vom avea mai multe thread-uri care consuma din aceeasi sursa ceea ce ar putea conduce la situatii cind aceeasi valoare este consumata de mai multe ori. Iata in continuare un exemplu de concurenta intre Consumatori:

Consumer1 Consumer2 Producer

while(!Ready)

while(!Ready)

this.Data = Data;

Taken = false;

Ready = true

Data = this.Data;

Data = this.Data;

Ready = false;

Taken = true;

return Data;

Ready = false;

Taken = true;

return data;

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.

Daca nici un thread nu a preluat monitorul, apelarea unei astfel de metode are ca efect preluarea acestui monitor. Achizitionarea monitorului este o operatie formata dintr-o singura instructiune care garanteaza faptul ca un singur thread va prelua acel monitor.

Sincronizarea utilizand monitoare

In urmatorul exemplu al clasei MyDatase foloseste metode de tip synchronized:

class MyData

public synchronized void store(int Data)

public synchronized int load()

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.

Celalalt thread nu va mai fi niciodata capabil sa-si execute propriul cod deoarece nu poate prelua monitorul. Doua thread-uri sunt in stare de interblocare cind unul dintre ele asteapta ca o valoare sa se schimbe in timp ce-l impiedica pe celalalt sa modifice acea valoare. Lucrul de care avem nevoie este de a prelua monitorul dupa ce a asteptat ca un flag sa se modifice. Solutia este de a folosi cuvintul cheie synchronized in asa fel incit sa protejam numai segmentul de cod critic care are nevoie de excludere mutuala:

class MyData

public void store(int Data)

}

public int load()

}

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 'trezit' de un apel al metodei notify(). Cind un thread isi termina 'asteptat-ul' el va recapata monitorul. Metoda notify() alege in mod arbitrar un thread din cele care sunt in asteptare si il elibereaza de aceasta stare. Metodele wait() si notify() sunt utilizate in clasa MyData dupa cum urmeaza:

class MyData

public synchronized void store(int Data) catch (InterruptedException e)

this.Data=Data;

Ready=true;

notify();

}

public synchronized int load() catch (InterruptedException e)

Ready=false;

notify();

return this.Data;

}

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(). Acest notify() va 'trezi' alt thread care va gasi ca nu exista nimic de citit pentru moment si astfel se va reintoarce in wait() eliberind din nou monitorul. Producatorul va fi din nou capabil sa-l preia si sa emita un nou numar.

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 'trezi' pe fiecare dintre ei la un anumit timp permitindu-le sa genereze noi date. Deoarece Ready este false dupa ce consumatorul 'consuma' fiecare valoare, un singur thread producator va produce o singura valoare. Deoarece producatorul va schimba starea flag-ului Ready ceilalti producatori nu vor suprascrie o noua valoare (chiar daca acestia sunt 'treji') pina ce aceasta valoare nu este consumata.

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

public synchronized void Reached() catch (InterruptedException e)

} else

}

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 'trezit' ei intra in competitie pentru monitor si unul dintre ei il cistiga, apoi iese imediat eliberind monitorul. Celelalte thread-uri continua sa concureze pentru acest monitor si sa-l elibereze pina ce ultimul thread face acest lucru si-n acest moment sunt cu totii 'liberi'. In acest exemplu am presupus ca se cunoaste dinainte numarul de thread-uri participante. Daca, insa, acest numar nu este cunoscut dinainte poate fi aplicat un mecanism de inregistrare a participantilor. Orice thread care doreste sa astepte la bariera, mai intii trebuie sa se 'inregistreze' incrementind numarul de thread-uri in asteptare. De unde stim ca toate thread-urile care asteapta la bariera s-au inregistrat? Ce se intimpla daca toate thread-urile mai putin unul si-au terminat de facut calculele si ultimul thread (care, probabil, nici nu a fost planificat sa ruleze inca) nu s-a inregistrat inca. Toate celelalte thread-uri vor trece bariera nestiind ca trebuie sa-l astepte si pe acesta din urma.

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 'arunca' exceptia InterruptedException. Iata un exemplu de program in care un thread asteapta un alt thread sa se termine:

Class MainThread extends Thread

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

Semaphore()

public synchronized void Get()

public synchronized void Put()

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.

Aplicatia 1: Algoritmul multisectiune

In aceasta anexa va prezentam un exemplu practic de utilizare a thread-urilor. Vom implementa un algoritm multisectiune in Java. Un algoritm multisectiune este varianta paralela a algoritmului bisectiune sau (bisection). Ideea este urmatoarea: vrem sa aflam punctele unde functia f(x), continua pe un interval, trece prin valoarea 0 (zero). Se da un interval [a,b] si stim ca functia va trece o singura data prin valoarea zero in acest interval. Mai intii calculam f(a)*f(b). Daca rezultatul este mai mare ca zero, vom sti ca functia nu trece prin valoarea zero si se returneaza esec. Daca rezultatul este zero se verifica valorile f(a) si f(b), una din ele fiind sigur zero, deci avem o solutie.

Altfel divizam intervalul in p subintervale si se trece la executarea algoritmului pentru fiecare subinterval rezultat. Apoi se trece la executarea algoritmului asupra subintervalului care va returna succes. In continuare este prezentat algoritmul multisectiune in care apare un mecanism de sincronizare foarte asemanator mecanismului producator/consumator.

import java.lang.Math;   // pentru functia abs()

class Bis

abstract class Function

class Bisection implements Runnable

public void run ()

if (fmin == 0)

if (fmax == 0)

if (Math.abs(max-min) < e)

// De aici stim ca trebuie impartit in p parti

// si startat cite un thread pentru fiecare

Bisection sub[] = new Bisection[p];

Thread t[] = new Thread[p];

for (int i=0;i<p;i++)

// Asteptam ca aceste thread-uri sa se termine

// Multe o vor face instantaneu, dar unul nu

Status = false;

for (int i=0;i<p;i++)

if (sub[i].waitForMe())

mynotify(); // Punem rezultatul in Result.

}

public double GetResult()

synchronized void mynotify()

catch (InterruptedException e)

}

}

public synchronized boolean waitForMe()

catch (InterruptedException e)

}

Done=false;

notify(); // Trezeste mynotify() in asteptare..

return (Status);

}

Aplicatia 2: Inmultire matrici

Vom realiza in continuare un program care calculeaza suma a doua matrice. Aceasta suma este calculata in paralel pe fiecare linie in parte. Mai pentru a calcula suma C=A+B, A(nxm),B(nxm),C(nxm) vom construi n thread-uri, iar fiecare dintre ele calculeaza suma dintre o linie i a matricei A si linia i a matricei B.

//adunarea a doua matrice

//liniile se aduna in paralel

import java.io.*;

class adunaLinie extends Thread

public void run()

}

public class adunaMatrice

, };

double m_b[][]= , , };

int n=m_a.length;//nr linii

int m=m_a[0].length;//nr coloane

double m_rez[][]=new double[n][m];

adunaLinie fir[]=new adunaLinie[n];

int i,j;

for (i=0;i<n;i++)

for(i=0;i<n;i++)

try

catch(InterruptedException e)

//afisare matrice rezultat

System.out.println();

for(i=0;i<n;i++)

}

Tema

Intr-un birou sunt un numar de functionari care din cand in cand tiparesc la imprimanta documente; nu toti elaboreaza documente in acelasi ritm. Deoarece au o singura imprimanta in birou, la imprimanta poate tipari doar o singura persoana la un moment dat. Sa se simuleze functionare biroului. (mai multi producatori - un consumator)

2. Intr-o vama sunt 4 puncte de trecere. Tirurile care sosesc la frontiera trebuie sa treaca prin unul din aceste puncte. In acest sens se aseaza la una din cozi (la cea mai scurta). Punctele care realizeaza controlul nu au acelasi ritm de verificare (depinzind de continutul transportului). Sa se simuleze functionarea vamii. (un producator - mai multi consumatori)





Politica de confidentialitate





Copyright © 2024 - Toate drepturile rezervate