Documente Academic
Documente Profesional
Documente Cultură
Multi Threading
Multi Threading
1.1
Pentru a gsi n program locurile unde se pot folosi threaduri trebuie depistate
proprietile:
Paralelism
Operaii I/O suprapuse
Evenimente asincrone
Planificare real-time
Oricnd un task are una dintre aceste proprieti, el este un candidat pentru a rula ntr-un
thread. Un astfel de task se poate identifica folosind urmtorele criterii:
Este independent de alte taskuri. Folosete resurse diferite de cele folosite de alte taskuri?
Execuia sa depinde de rezultatele altor taskuri? Cu ct taskurile sunt mai strnse n
interdependen, cu att crete ansa ca ele s sfreasc prin a se bloca n ateptare
reciproc.
Poate fi blocat n ateptri lungi. Poate taskul s rmn suspendat un timp ndelungat?
Un program poate efectua milioane de operaii cu ntregi n timpul n care face o singur
operaie de I/O. Dedicarea unui thread pentru I/O poate spori considerabil performanele
unui program.
Poate folosi multe cicluri CPU. Execut taskul calcule complexe (asupra matricelor,
dispersie, criptare)? Calculele consumatoare de timp i indepenente de alte activiti sunt
buni candidai pentru threading.
Trebuie s rspund la evenimente asincrone. Trebuie taskul s trateze evenimente
asincrone ce apar la intervale de timp aleatorii ,ca, de exemplu, comunicarea n reea?
threadurile sunt soluia pentru a ncapsula i sincroniza serviciile acestor evenimente.
Operaille executate n thread au mai mic sau mai mare importan dect operaiile
efectuate n alte threaduri. Sarcina trebuie executat ntr-un timp specificat? Trebuie s se
execute la intervale precizate? Consideraiile legate de planificare sunt adesea motive
bune pentru mulithreading. De exemplu, o aplicaie window manager va asigna o
prioritate mai mare operaiilor de input de la utilizator dact threadului garbage
collection.
2
Programele server care rspund continuu la evenimente asincrone sunt aplicaii ideale
pentru threading. Aplicaiile de calcul i semnalizare (signal-processing) sunt alte
candidate pentru multithreading. Ele conin taskuri care pot fi executate pe mai multe
procesoare. n sfrit, aplicaiile n timp real sunt i ele ideale pentru multithreading. Ele
sunt mult mai eficiente dect soluiile oferite de multi-proces. Modelul cu threaduri
permite stabilirea de prioriti i elimin din complexitatea ce apare la programerea
asincron.
1.2
Nu exist reguli fixe pentru programarea multithreading. Fiecare program are propriul lui
specific. Totui, de-a lungul timpului, au aprut cteva modele care specific n ce fel o
aplicaie multithreading mparte sarcinile threadurilor i cum comunic acestea. In cele ce
urmeaz se vor discuta:
1.3
Modelul Boss/Worker
Resurse:
Task X
Input Stream
Boss
main()
Fiiere
Task Y
Baze de date
Diskuri
Task Z
Device-uri speciale
3
Boss-ul creeaz pe fiecare din threadurile worker, le asigneaz taskuri, i eventual
ateapt s se termine execuia lor. n pseudocodul din Exemplul ? boss-ul creeaz n mod
dinamic cte un worker atunci cnd primete o nou cerere. n procedura pthread_create
boss-ul specific rutina legat de taskul ce l va executa threadul. Dup creare boss-ul
ateapt o nou cerere.
Exemplul ? Modelul boss/worker
main() {
forever {
primeste o cerere
switch cerere
case cerereX: thread_create(..taskX);
case cerereY: thread_create(..taskY);
- - }
}
taskX() {
execut taskul, iar n caz de acces la resurse partajate
f sincronizare
}
- - -
Dac boss-ul creeaz threadurile n mod dinamic, atunci vor exista attea threaduri
concurente cte cereri concurente au sosit. Ca o alternativ, boss-ul poate economisi
suprancrcarea (overhead) ce apre la execuie,dac creeaz n prealabil taskurile. Soluia
poart denumirea de thread pool (Exemplul ?) Boss-ul creeaz threadurile la iniializare.
Aceste se suspend imediat i vor fi repornite de apeluri de la boss. Boss-ul folosete o
coad unde pune cererile primite. Ele vor fi tratate de ctre threaduri.
Exemplul ? Modelul boss/worker cu pool
main() {
for numrul de workers
thread_create(. . .pool_base . . .)
forever {
primete o cerere
plaseaz cererea n coad
semnaleaz threadurile n ateptare c este de lucru
}
pool_base() {
sleep until boss semnaleaz c este de lucru
scoate o cerere din coad
4
switch cerere {
case cerereX: taskX();
case cerereY: taskY();
- - }
}
Spre deosebire de primul model prezentat, aici threadurile lucreaz concurent asupra
taskurilor fr s existe un leader. Un thread este rspunztor cu crearea celorlalte
threaduri (egale) la pornirea programului, apoi acest thread se va comporta ca un thread
obinuit devenind egal cu celelalte. El va trata cereri sau va atepta terminarea celorlalte
threaduri.
Input
(static)
Program
Workers
Resurse:
Task X
Task Y
Fiiere
Baze de date
Task Z
Diskuri
Device-uri
speciale
5
main() {
thread_create(thread1task1)
thread_create(thread2task2)
- - Anun toi workers s execute start
Ateapt s se termine toi workers
F eventuale aciuni de terminare
}
task1() {
Ateapt semnal de start
execut taskul, iar n caz de acces la resurse partajate
f sincronizare
}
- - -
Modelul peer este adecvat pentru aplicaii care au o mulime fix sau bine definit de date
de intrare, cum ar fi nmulirea de matrice, generatoare de numere prime, maini de
cutare paralel. Pentru c nu exist un bos, threadurile peer threbuie s i sincronizeze
accesul la sursele de input comune. Ca i n cazul precedent, poate aprea ncetinire
(slowdown) dac sincronizarea se face prea de des.
Fie o aplicaie n care un plan sau un spaiu este divizat pe mai multe threaduri astfel nct
s poate calcula schimbrile de temperatur n timp ce este radiat cldur pe suprafa.
Fiecare thread poate calcula un delta de schimbare. Pentru c rezultatele calculelor
fiecrui thread necesit o ajustare a limitelor pentru threadul urmtor, toate threadurile
threbuie s se sincronizeze i s compare rezultatele. Acesta este un exemplu clasic
pentru o aplicaie peer.
1.5
Modelul pipeline
6
Dup cum ilustreaz pseudocodul din Exemplul ?, un thread primete inputul pentru
ntregul program i l trimite primului filtru. De asemenea un singur thread colecteaz
rezultatul i produce outputul programului. Fiecare thread proceseaz inputul primit de la
threadul anterior lui (ca ordine). Modelul este util n prelucrearea de imagini i de texte.
Filtre
Filtru 1
Input Stream
Resurse
Filtru 2
..
Filtru 3
..
Fiiere
Fiiere
Fiiere
Baze de date
Baze de date
Baze de date
Diskuri
Diskuri
Diskuri
Device-uri
speciale
Device-uri
speciale
Device-uri
speciale
- - }
thread_create(stage1)
thread_create(stage2)
wait s se termine toate threads
stage1() {
forever {
citete intrarea pentru program
execut stadiul 1 al prelucrrii intrrii
trece rezultatele ieirii lui spre stadiul 2
}
}
7
stage2() {
forever {
citete intrarea primit de la stagiul 1
execut stagiul2 al prelucrrii
trece rezultatele ieirii lui spre stagiul 3
}
}
- - -
8
{
int
i, j, n = ARRAY_SIZE;
matrix_t MA, MB, MC;
matrix_work_order_t *work_orderp;
pthread_t peer[size*size];
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
id = j * i *10;
work_order_p = (work_order_t *)malloc(sizeof(work_order_t));
work_order_p->id = id;
work_order_p->size = n;
work_order_p->i = i;
work_order_p->j = j;
work_order_p->MA = &MA;
work_order_p->MB = &MB;
work_order_p->MC = &MC;
pthread_create(&(peer[id]), NULL, (void *)peer_mult, (void
*)work_orderp);
}
}
/* Asteapta dupa peers sa se termine */
for(i=0; i < (n * n); i++) {
pthread_join(peer[i], NULL);
}
- - -
In exemplul serial programul principal apela o rutin mult care facea nmulirea. Ea
avea mai multe argumente, iar pthread_create poate fi aplelat doar asupra unei
rutine cu un singur argument. Soluia const n folosirea unei structuri pentru transmitera
de paramentrii i a unei rutine auxiliare. Toate datele ce trebuie transmise threadurilor
sunt mpachetate ntr-o structur , matrix_work_order_t. Fiecrui thread trebuie s
i se transmit o structur unic! Exist pericolul a dou abordri greite: folosirea unei
structuri statice i plasarea lui malloc n afara ciclului. Aceasta ar cauza suprascrierea
structurii. Rutina auxiliar are ca parametru structura menionat i apeleaz rutina mult
care are mai muli parametrii (i deci nu poate fi folosit n pthread_create).
Exemplul ? rutina peer_mult
void peer_mult( matri_work_order_t *word_orderp)
{
mult( work_orderp->i,
work_orderp->j,
*(work_orderp->MA),
*(work_orderp->MB),
*(work_orderp->MC);
}
Instrumente de sincronizare
In vederea sincronizrilor se pot alege mai multe soluii din numeroase funcii Pthread
disponibile.
Pthread_join. Aceast funcie suspendeaz execuia unui thread pn la terminarea
unui alt thread.
Variabile Mutex. O variabil mutex acioneaz ca o blocare exclusiv, mutual,
permitnd threadului s controleze accesul la date. La un moment dat numai un singur
thread poate s blocheze i s aib acces la data protejat.
Variabile condiionale. O variabil condiional ne d o posibilitate de denumire a
evenimentelor n care thread-urile sunt interesate. Un eveniment poate fi chiar i
faptul c un contor ajunge la o valoare prestabilit, sau un flag este setat sau anulat;
dar poate fi i mai complex implicnd o coinciden specific a mai multor
evenimente. Thread-urile sunt interesate n aceste evenimente, deoarece asemenea
evenimente nseam c s-a mplinit o condiie, care le permite s continue cu o
anumit faz a execuiei lor. Biblioteca de funcii Pthreads asigur ci pentru threaduri att penru a exprima interesul lor n mplinirea unei condiii, ct i pentru a
semnaliza mplinirea condiiei ateptat.
Pthread_once. Funcia este o unealt specializat de sincronizare care garanteaz c
rutinele de iniializare se execut o dat i numai o dat, cnd sunt apelate de threaduri multiple.
Aceste unelte de sincronizare ne asigur tot ce este necesar pentu a scrie aproape
orice program care se poate imagina. Mai mult, se pot creea instrumente de sincronizare
orict de complexe folosind numai aceste elemente de construcii. Cteva dintre aceste
mecanizme de sincronizare sunt:
10
11
Mutex-urile sunt foarte bune la controlul accesului direct la date i resurse. Cu
toate c putem folosi mutex-urile pentru a construi blocuri complexe pentru mecanisme
de sincronizare, Pthread ofer adesea instrumente mai potrivite pentru acesta.
n caz particular, un task n programarea thread este alctuit din sincronizri de
evenimente. Fiecare thread din program poate s ajung ntr-un punct unde trebuie s
atepte pn la terminarea altor threaduri. n asemenea cazuri putem folosi variabile
condiionale sau contoare prin care definim nite bariere pentru threaduri. Nu fiecare
thread trebuie s decrementeze aceste contoare cnd blocheaz un mutex, dar uneori este
necesar s folosim mai multe blocri pentru un mutex ca acest contor s ajunge la zero.
Dac vrem s folosim contoare pentru a determina dac un thread este sincronizat cu nu
eveniment, atunci putem folosi variabile condiionale.
Mutex-urile sunt cele mai restrictive tipuri ale controlului de acces. Cnd un
thread blocheaz un mutex pe o resurs, interzice celorlalte thread-uri accesul la resurs.
n unele cazuri nu aceast metod este cea mai eficient pentru sincronizare.
Sunt cazuri n care avem mai multe thread-uri care citete data respectiv, i un
singur thread care scrie. n asemenea situaii este mai bine s folosim blocri normale la
citiri i mutex-uri la scriere.
Exist cazuri cnd avem nevoie de blocri recursive. Mutex-urile nu sunt pregtite
pentru asemenea situaii. Putem imagina un contor intern, care se incrementeaz la
fiecare blocare recursiv i se decrementeaz la fiecare deblocare, iar deblocarea
definitiv se ntmpl n momentul cnd acest contor interior ajunge la zero.
Cnd mai multe thred-uri ateapt pentru deblocarea unui mutex, ce thread are
acces la acesta imediat dup deblocare? Soluia este acordarea de prioriti pentru
threaduri. Threadul cu cea mai mare prioritate primeste acces la blocare. Exist programe
n care accesarea este fcut n mod aleator.
Folosirea prioritiilor n programea multithreading poate conduce la o problem
multiprocesor clasic: inversia prioritiilor. Aceast situaie apare cnd un thread cu
prioritate inferioar deine blocarea unei resurse pe care o ateapt un thread cu prioritate
superior. Pentru c threadul cu prioritate mare nu poate s continu activitatea, ateptnd
deblocarea, fiecare thread este tratat cum ar avea prioritatea inversat. Mutexurile pot
controla asemea situaii.
12
conjuncie
cu
extensia
POSIX
real-time,
trebuie
_POSIX_THREAD_PRIORITY_SCHEDULING la TRUE.
setat
constanta
13
permite ca planificarea threadurilor s fie facut att la nivel de proces, ct i la nivel de
sistem.
n cazul planificarii la nivel de proces, threadurile concureaz ntre ele n cadrul
aceluiai program. Dac planificarea se face la nivel de sistem, threadurile concureaz cu
altele(active) din cadrul sistemului. Pentru a stabili nivelul la care se face planificarea,
exist pentru un thread un atribut care permite setarea acestuia.
Planificarea threadurilor devine mai complicat n cazul n care sunt implicate
sisteme multiprocesor. Gruparea logic a mai multor procesoare n scopul planificrii
threadurilor se constituie, dup standardele Pthreads, ntr-un domeniu de alocare
(Allocation Domain). Dimensiunile acestor domenii nu sunt nsa stabilite strict (pot fi
stabilite de programatori).
Figura ? arat modul n care lucreaz un sistem care utilizeaz planificarea la
nivel de sistem, cu un singur domeniu de alocare.
Procesul A
Schedule
r
Threaduri
le
Considera
Domeniu de
alocare
Procesul B
Thread(uri)
selectate
CPU
s
Procesul C
14
O situatie puin diferit apare n cazul planificrii la nivel de proces. E nevoie de un
planificator care s respecte planificarea doar n zona procesului, i care s compar
prioritile unui thread cu a altora doar la nivelul unui singur proces. Ca rezultat, s-ar
putea ca prioritile stabilite la un moment dat la nivel de proces s nu mai aib acelasi
sens in la nivel de sistem.
Urmrind figura ?, se poate observa modul de lucru n acest caz. Presupunem c
procesul A are trei threaduri, unul de prioritate maxim, celelalte cu prioritate medie.
Planificatorul poate plasa threadul cu prioritate maxim unui procesor disponibil, i astfel
se rezolv problema n cel mai simplu mod. n procesul A, n timp ce acesta se execut
prin threadul cu prioritate maxim, celelate doua asteapt momentul n care vor intra i
ele n execuie. ns planificatorul nu mai caut alte threaduri din afara acestui proces,
care sunt n faza de rulare i care ar avea prioritate mai mic, pentru a elibera
procesoarele alocate lor i pentru a rula aceste threaduri, care au o prioritate mai mare
(dei nu maxim). Aceasta nseamn o folosire inadecvat a resurselor sistemului, care
poate dezavantaja uneori execuia programelor.
Procesul A
Procesul B
Schedule
r
Threadurile Threaduri
le
considerate
la nivel de considerat
proces e la nivel
de sistem
Domeniu de
alocare
Thread(uri)
selectate
CPU
s
Procesul C
15
- threadurile procesului B sunt planificate la nivel de sistem i au , de asemenea,
propriul lor domeniu de alocare;
- presupunem c toate celelalte procese care ar mai putea exista n sistem au
threadurile planificate la nivel de sistem i le e alocat domeniul ramas disponibil.
Deoarece procesele A i B nu mpart nici un domeniu de alocare cu alte procese,
ele se vor executa cel mai probabil. Threadurile lor nu vor atepta niciodat dup alte
procese sau threaduri ale acestora. n plus, pentru aceste dou procese sunt valabile toate
proprietile specifice tipului lor de planificare (amintite mai sus).
Threaduri n execuie i threaduri blocate
n momentul n care se selecteaz un thread, planificatorul trebuie s tie despre el
dac se poate executa imediat sau este blocat. Dup ce au fost determinate threadurile
blocate, planificatorul trebuie s selecteze un thread care se poate executa imediat.
Algoritmul de planificare trebuie aplicat doar n cazul n care numarul sloturilor de
procesare disponibile este mai mic dect numarul threadurilor.
Scheduling Priority
Algoritmul de selecie folosit de planificator depinde de prioritatile de planificare
i de scheduling policy a fiecarui thread.
Planificatorul cerceteaz pentru nceput un ir de cozi de prioritate(vezi figura ?).
Se gestioneaz o coada pentru fiecare prioritate de planificare i, la un nivel dat de
prioritate, threadurile asignate acelui tip de prioritate. n momentul n care planificatorul
caut un thread pentru a-l pune n executie, ncepe cu coada prioritii celei mai mari,
continund ca nivelele prioritilor inferiore, pn gsete primul thread.
Fig. 3 Cozi de prioritate
Threaduri blocate
Planificator
Threaduri executabile
Ma
x
Procesor
Min
16
17
O problem real i deloc placut este aceea a inversrii prioritilor a doua threaduri.
Pthreads standard permite (dar nu e neaprat necesar) crearea de mutexuri care pot da
priority boost threadurilor de mica prioritate care le blocheaz(pe mutexuri). Mutexului i
poate fi asociat oricare din urmtoarele doua protocoale de prioritate pe care le ofer
acest mecanism : stabilirea unui plafon de prioritate sau motenirea de prioritate.
Plafonul de prioritate
Acest protocol asociaza unui mutex o prioritate planificabil. Astfel echipat, un
mutex i poate asigna threadului care l deine o prioritate efectiv egal cu a lui, dac
threadul respectiv avea o prioritate mai mic la nceput. Iata i secventele de creare a unui
astfel de mutex.
- se creaz nti un obiect atribut al mutexului (pthread_mutex_attr_t) i se
iniializeaz cu pthread_mutexattr_init.
- se obine protocolul de prioritate asociat implicit mutexului. Acesta poate fi:
PTHREAD_PRIO_NONE : mutexul nu are un protocol de prioritate;
PTHREAD_PRIO_PROTECT : mutexul utilizeaz protocolul plafonului de prioritate;
PTHREAD_PRIO_INHERIT : mutexul utilizeaz protocolul motenirii de prioritate;
- daca mutexul nu folosete protocolul plafonului de prioritate, acesta va fi setat
(pthread_mutexattr_setprotocol);
- se apeleaz pthread_mutexattr_setprioceiling pentru a seta atributul de
prioritate n cadrul obiectului mutex;
- se iniializeaz mutexul prin specificarea i a obiectului atribut al mutexului;
Motenirea de prioritate
Acest protocol permite unui mutex s schimbe prioritatea threadului care l-a
blocat la aceea a threadului cu cea mai mare prioritate care se afla n coada de ateptare.