Programare Multi Threading

56
Programare multithreading - 1 2 PROGRAMARE MULTITHREADING ................................................................................................................ 2 2.1 DEFINIŢII ..................................................................................................................................................................... 2 2.1.1 Definirea procesului .......................................................................................................................................... 2 2.1.2 Reprezentarea în memorie a unui proces .......................................................................................................... 2 2.1.3 Definiţia threadului ........................................................................................................................................... 4 2.2 METODOLOGIA ŞI PROBLEMATICA UTILIZĂRII ŞI SINCRONIZĂRII THREADURILOR ........................................................ 5 2.2.1 Variabile mutex ................................................................................................................................................. 5 2.2.2 Variabile condiţionale ....................................................................................................................................... 6 2.2.3 Conceptul de monitor ........................................................................................................................................ 6 2.3 RELAŢII THREAD PROGRAM SISTEM DE OPERARE ................................................................................................... 7 2.3.1 Contextul şi stările unui thread ......................................................................................................................... 7 2.3.2 Tipuri de thread-uri........................................................................................................................................... 8 2.3.2.1 O clasificare a thread-urilor ............................................................................................................................................ 8 2.3.2.2 Thread-uri nucleu ........................................................................................................................................................... 9 2.3.2.3 Thread-uri utilizator ..................................................................................................................................................... 10 2.4 EXEMPLE DE PROBLEME REZOLVABILE PRIN THREAD-URI ......................................................................................... 11 2.4.1 Adunarea în paralel a n numere ...................................................................................................................... 11 2.4.2 Problema producătorului şi consumatorului .................................................................................................. 13 2.4.3 Problema cititorilor şi a scriitorilor ............................................................................................................... 14 2.5 THREAD-URI PE PLATFORME UNIX: POSIX ŞI SOLARIS ............................................................................................... 16 2.5.1 Caracteristici şi comparaţii Posix şi Solaris ................................................................................................... 16 2.5.1.1 Contextul şi stările unui thread ..................................................................................................................................... 16 2.5.1.2 Caracteristici ale thread-urilor sub Linux ..................................................................................................................... 16 2.5.1.3 Caracteristici ale thread-urilor sub Solaris.................................................................................................................... 17 2.5.1.4 Similarităţi şi facilităţi specifice ................................................................................................................................... 17 2.5.2 Operaţii asupra thread-urilor: creare, terminare ........................................................................................... 18 2.5.2.1 Crearea unui thread ...................................................................................................................................................... 18 2.5.2.2 Terminarea unui thread şi aşteptarea terminării lui ...................................................................................................... 19 2.5.2.3 Un prim exemplu .......................................................................................................................................................... 21 2.5.3 Instrumente standard de sincronizare ............................................................................................................. 23 2.5.3.1 Operaţii cu variabile mutex .......................................................................................................................................... 23 2.5.3.2 Operaţii cu variabile condiţionale................................................................................................................................. 25 2.5.3.3 Operaţii cu semafoare ................................................................................................................................................... 28 2.5.3.4 Blocare de tip cititor / scriitor (reader / writer) ............................................................................................................. 29 2.5.4 Exemple de sincronizări .................................................................................................................................. 30 2.5.5 Obiecte purtătoare de atribute Posix .............................................................................................................. 32 2.5.5.1 Iniţializarea şi distrugerea unui obiect atribut ............................................................................................................... 33 2.5.5.2 Gestiunea obiectelor purtătoare de atribute thread ....................................................................................................... 34 2.5.5.3 Gestiunea obiectelor purtătoare de atribute mutex, cond, rdwr .................................................................................... 35 2.5.6 Planificarea thread-urilor sub Unix................................................................................................................ 36 2.5.6.1 Gestiunea priorităţilor sub Posix .................................................................................................................................. 36 2.5.6.2 Gestiunea priorităţilor sub Solaris ................................................................................................................................ 37 2.5.6.3 Exemplu de planificare manuală a thread-urilor pe Solaris .......................................................................................... 37 2.5.7 Facilităţi speciale ale lucrului cu thread-uri Unix .......................................................................................... 38 2.5.7.1 Execuţie cel mult o dată ............................................................................................................................................... 38 2.5.7.2 Asociere date specifice thread-urilor cu chei ................................................................................................................ 38 2.5.7.3 Operaţii specifice lwp sub Solaris ................................................................................................................................ 41 2.5.8 Exemple clasice de lucru cu thread-uri ........................................................................................................... 43 2.5.8.1 Adunarea în paralel a n numere .................................................................................................................................... 43 2.5.8.2 Problema producătorilor şi a consumatorilor................................................................................................................ 45 2.5.8.3 Problema cititorilor şi a scriitorilor .............................................................................................................................. 47 2.6 THREAD-URI PE PLATFORME MICROSOFT: WINDOWS NT, 2000 ................................................................................ 49 2.6.1 Caracteristici ale thread-urilor sub Windows NT ........................................................................................... 49 2.6.2 Operaţii asupra thread-urilor: creare, terminare ........................................................................................... 50 2.6.2.1 Crearea unui thread ...................................................................................................................................................... 50 2.6.2.2 Terminarea unui thread ................................................................................................................................................ 51 2.6.3 Instrumente standard de sincronizare ............................................................................................................. 51 2.6.3.1 Funcţii de aşteptare....................................................................................................................................................... 52 2.6.3.2 Variabile mutex ............................................................................................................................................................ 52 2.6.3.3 Semafoare fără nume .................................................................................................................................................... 53 2.6.3.4 Secţiuni critice .............................................................................................................................................................. 53 2.6.3.5 Alte obiecte de sincronizare ......................................................................................................................................... 54 2.6.4 Atributele şi planificarea thread-urilor NT ..................................................................................................... 54 2.6.4.1 Atributele thread-urilor................................................................................................................................................. 54 2.6.4.2 Priorităţile thread-urilor................................................................................................................................................ 55

description

Programare Multi Threading

Transcript of Programare Multi Threading

Page 1: Programare Multi Threading

Programare multithreading - 1 –

2 PROGRAMARE MULTITHREADING ................................................................................................................ 2

2.1 DEFINIŢII ..................................................................................................................................................................... 2 2.1.1 Definirea procesului .......................................................................................................................................... 2 2.1.2 Reprezentarea în memorie a unui proces .......................................................................................................... 2 2.1.3 Definiţia threadului ........................................................................................................................................... 4

2.2 METODOLOGIA ŞI PROBLEMATICA UTILIZĂRII ŞI SINCRONIZĂRII THREADURILOR ........................................................ 5 2.2.1 Variabile mutex ................................................................................................................................................. 5 2.2.2 Variabile condiţionale ....................................................................................................................................... 6 2.2.3 Conceptul de monitor ........................................................................................................................................ 6

2.3 RELAŢII THREAD – PROGRAM – SISTEM DE OPERARE ................................................................................................... 7 2.3.1 Contextul şi stările unui thread ......................................................................................................................... 7 2.3.2 Tipuri de thread-uri ........................................................................................................................................... 8

2.3.2.1 O clasificare a thread-urilor ............................................................................................................................................ 8 2.3.2.2 Thread-uri nucleu ........................................................................................................................................................... 9 2.3.2.3 Thread-uri utilizator ..................................................................................................................................................... 10

2.4 EXEMPLE DE PROBLEME REZOLVABILE PRIN THREAD-URI ......................................................................................... 11 2.4.1 Adunarea în paralel a n numere ...................................................................................................................... 11 2.4.2 Problema producătorului şi consumatorului .................................................................................................. 13 2.4.3 Problema cititorilor şi a scriitorilor ............................................................................................................... 14

2.5 THREAD-URI PE PLATFORME UNIX: POSIX ŞI SOLARIS ............................................................................................... 16 2.5.1 Caracteristici şi comparaţii Posix şi Solaris ................................................................................................... 16

2.5.1.1 Contextul şi stările unui thread ..................................................................................................................................... 16 2.5.1.2 Caracteristici ale thread-urilor sub Linux ..................................................................................................................... 16 2.5.1.3 Caracteristici ale thread-urilor sub Solaris.................................................................................................................... 17 2.5.1.4 Similarităţi şi facilităţi specifice ................................................................................................................................... 17

2.5.2 Operaţii asupra thread-urilor: creare, terminare ........................................................................................... 18 2.5.2.1 Crearea unui thread ...................................................................................................................................................... 18 2.5.2.2 Terminarea unui thread şi aşteptarea terminării lui ...................................................................................................... 19 2.5.2.3 Un prim exemplu .......................................................................................................................................................... 21

2.5.3 Instrumente standard de sincronizare ............................................................................................................. 23 2.5.3.1 Operaţii cu variabile mutex .......................................................................................................................................... 23 2.5.3.2 Operaţii cu variabile condiţionale ................................................................................................................................. 25 2.5.3.3 Operaţii cu semafoare ................................................................................................................................................... 28 2.5.3.4 Blocare de tip cititor / scriitor (reader / writer) ............................................................................................................. 29

2.5.4 Exemple de sincronizări .................................................................................................................................. 30 2.5.5 Obiecte purtătoare de atribute Posix .............................................................................................................. 32

2.5.5.1 Iniţializarea şi distrugerea unui obiect atribut ............................................................................................................... 33 2.5.5.2 Gestiunea obiectelor purtătoare de atribute thread ....................................................................................................... 34 2.5.5.3 Gestiunea obiectelor purtătoare de atribute mutex, cond, rdwr .................................................................................... 35

2.5.6 Planificarea thread-urilor sub Unix ................................................................................................................ 36 2.5.6.1 Gestiunea priorităţilor sub Posix .................................................................................................................................. 36 2.5.6.2 Gestiunea priorităţilor sub Solaris ................................................................................................................................ 37 2.5.6.3 Exemplu de planificare manuală a thread-urilor pe Solaris .......................................................................................... 37

2.5.7 Facilităţi speciale ale lucrului cu thread-uri Unix .......................................................................................... 38 2.5.7.1 Execuţie cel mult o dată ............................................................................................................................................... 38 2.5.7.2 Asociere date specifice thread-urilor cu chei ................................................................................................................ 38 2.5.7.3 Operaţii specifice lwp sub Solaris ................................................................................................................................ 41

2.5.8 Exemple clasice de lucru cu thread-uri ........................................................................................................... 43 2.5.8.1 Adunarea în paralel a n numere .................................................................................................................................... 43 2.5.8.2 Problema producătorilor şi a consumatorilor ................................................................................................................ 45 2.5.8.3 Problema cititorilor şi a scriitorilor .............................................................................................................................. 47

2.6 THREAD-URI PE PLATFORME MICROSOFT: WINDOWS NT, 2000 ................................................................................ 49 2.6.1 Caracteristici ale thread-urilor sub Windows NT ........................................................................................... 49 2.6.2 Operaţii asupra thread-urilor: creare, terminare ........................................................................................... 50

2.6.2.1 Crearea unui thread ...................................................................................................................................................... 50 2.6.2.2 Terminarea unui thread ................................................................................................................................................ 51

2.6.3 Instrumente standard de sincronizare ............................................................................................................. 51 2.6.3.1 Funcţii de aşteptare ....................................................................................................................................................... 52 2.6.3.2 Variabile mutex ............................................................................................................................................................ 52 2.6.3.3 Semafoare fără nume .................................................................................................................................................... 53 2.6.3.4 Secţiuni critice .............................................................................................................................................................. 53 2.6.3.5 Alte obiecte de sincronizare ......................................................................................................................................... 54

2.6.4 Atributele şi planificarea thread-urilor NT ..................................................................................................... 54 2.6.4.1 Atributele thread-urilor ................................................................................................................................................. 54 2.6.4.2 Priorităţile thread-urilor ................................................................................................................................................ 55

Page 2: Programare Multi Threading

Programare multithreading - 2 –

2 Programare multithreading

2.1 Definiţii

2.1.1 Definirea procesului

Ce este un proces? Un proces sau task, este un calcul care poate fi executat concurent (în paralel)

cu alte calcule. El este o abstractizare a activităţii procesorului, fiind considerat ca un program în

execuţie. Existenţa unui proces este condiţionată de existenţa a trei factori:

o procedură - o succesiune de instrucţiuni dintr-un set predefinit de instrucţiuni, cu rolul

de descriere a unui calcul - descrierea unui algoritm.

un procesor - dispozitiv hardware/software ce recunoaşte şi poate executa setul predefinit

de instrucţiuni, şi care este folosit, în acest caz, pentru a executa succesiunea de

instrucţiuni specificată în procedură;

un mediu - constituit din partea din resursele sistemului: o parte din memoria internă, un

spaţiu disc destinat unor fişiere, periferice magnetice, echipamente audio-video etc. -

asupra căruia acţionează procesorul în conformitate cu secvenţa de instrucţiuni din

procedură.

Trebuie deci făcută deosebirea dintre proces şi program. Procesul are un caracter dinamic, el

precizează o secvenţă de activităţi în curs de execuţie, iar programul are un caracter static, el numai

descrie textual această secvenţă de activităţi.

Evoluţia în paralel a două procese trebuie înţeleasă astfel:

Dacă Ii şi Ij sunt momentele de început a două procese Pi şi Pj, iar Hi şi Hj sunt momentele lor de

sfârşit, atunci Pi şi Pj sunt executate concurent dacă:

max (Ii, Ij) <= min (Hi, Hj)

Grafic, pe o axă a timpului, dacă presupunem că procesul Pi începe primul şi se termină tot primul,

dar numai după ce a început Pj, evoluţia lor apare ca în figura 2.3

Figura 2.1 Timpul de concurenţă a două procese

Dacă sistemul dispune de două procesoare, atunci este posibil un paralelism efectiv, în sensul că atât

Pi cât şi Pj sunt executate simultan de câte un procesor. Dacă există un singur procesor, atunci

acesta execută alternativ grupuri de instrucţiuni din cele două procese. Decizia de comutare aparţine

sistemului de operare.

2.1.2 Reprezentarea în memorie a unui proces

In ceea ce priveşte reprezentarea în memorie a unui proces, indiferent de platforma (sistemul de

operare) pe care este operaţional, se disting, în esenţă, următoarele zone:

Ii Ij Hi Hj

. . . ___|_____|_______________________|__________|____ . .

\ timp de concurenţă /

Page 3: Programare Multi Threading

Programare multithreading - 3 –

Contextul procesului

Codul programului

Zona datelor globale

Zona heap

Zona stivei

Contextul procesului conţine informaţiile de localizare în memoria internă şi informaţiile de stare

a execuţiei procesului:

Legături exterioare cu platforma (sistemul de operare): numele procesului, directorul curent

în structura de directori, variabilele de mediu etc.;

Pointeri către începuturile zonelor de cod, date stivă şi heap şi, eventual, lungimile acestor

zone;

Starea curentă a execuţiei procesului: contorul de program (notat PC -program counter) ce

indică în zona cod următoarea instrucţiune maşină de executat, pointerul spre vârful stivei

(notat SP - stack pointer);

Zone de salvare a regiştrilor generali, de stare a sistemului de întreruperi etc.

De exemplu, în [15] se descrie contextul procesului sub sistemul de operare DOS, iar în [13]

contextul procesului sub sistemul de operare Unix.

Zona de cod conţine instrucţiunile maşină care dirijează funcţionarea procesului. De regulă,

conţinutul acestei zone este stabilit încă din faza de compilare. Programatorul descrie programul

într-un limbaj de programare de nivel înalt. Textul sursă al programului este supus procesului de

compilare care generează o secvenţă de instrucţiuni maşină echivalentă cu descrierea din

program.

Conţinutul acestei zone este folosit de procesor pentru a-şi încărca rând pe rând instrucţiunile de

executat. Registrul PC indică, în fiecare moment, locul unde a ajuns execuţia.

Zona datelor globale conţine constantele şi variabilele vizibile de către toate instrucţiunile

programului. Constantele şi o parte dintre variabile primesc valori încă din faza de compilare.

Aceste valori iniţiale sunt încărcate în locaţiile de reprezentare din zona datelor globale în momentul

încărcării programului în memorie.

Zona heap - cunoscută şi sub numele de zona variabilelor dinamice - găzduieşte spaţii de memorare

a unor variabile a căror durată de viaţă este fixată de către programator. Crearea (operaţia new) unei

astfel de variabile înseamnă rezervarea în heap a unui şir de octeţi necesar reprezentării ei şi

întoarcerea unui pointer / referinţe spre începutul acestui şir. Prin intermediul referinţei se poate

utiliza în scriere şi/sau citire această variabilă până în momentul distrugerii ei (operaţie destroy,

dispose etc.). Distrugerea înseamnă eliberarea şirului de octeţi rezervat la creare pentru

reprezentarea variabilei. In urma distrugerii, octeţii eliberaţi sunt plasaţi în lista de spaţii libere a

zonei heap.

In [13,47,2] sunt descrise mecanismele specifice de gestiune a zonei heap.

Zona stivă In momentul în care programul apelelează o procedură sau o funcţie, se depun în vârful

stivei o serie de informaţii: parametrii transmişi de programul apelator către procedură sau funcţie,

adresa de revenire la programul apelator, spaţiile de memorie necesare reprezentării variabilelor

locale declarate şi utilizate în interiorul procedurii sau funcţiei etc. După ce procedura sau funcţia îşi

încheie activitatea, spaţiul din vârful stivei ocupat la momentul apelului este eliberat. In cele mai

multe cazuri, există o stivă unică pentru fiecare proces. Există însă platforme, DOS este un exemplu

[14], care folosesc mai multe stive simultan: una rezervată numai pentru proces, alta (altele) pentru

Page 4: Programare Multi Threading

Programare multithreading - 4 –

apelurile sistem. Conceptul de thread, pe care-l vom prezenta imediat, induce ca regulă generală

existenţa mai multor spaţii de stivã.

In figura 2.4 sunt reprezentate douã procese active simultan într-un sistem de calcul.

Figura 2.2 Douã procese într-un sistem de calcul

2.1.3 Definiţia threadului

Conceptul de thread, sau fir de execuţie, a apãrut în ultimii 10-15 ani. Proiectanţii şi programatorii

au “simţit nevoia” sã-şi definească entităţi de calcul independente, dar în cadrul aceluiaşi proces.

Astfel, un thread se defineşte ca o entitate de execuţie din interiorul unui proces, compusă dintr-

un context şi o secvenţă de instrucţiuni de executat.

Deşi noţiunea de thread va fi prezentată pe larg în capitolele următoare, punctăm aici câteva

caracteristici de bază ale acestor entităţi:

Thread-urile sunt folosite pentru a crea programe formate din unităţi de procesare

concurentă.

Entitatea thread execută o secvenţă dată de instrucţiuni, încapsulate în funcţia thread-ului.

Execuţia unui thread poate fi întreruptă pentru a permite procesorului să dea controlul unui

alt thread.

Thread-urile sunt tratate independent, fie de procesul însuşi, fie de nucleul sistemului de

operare. Componenta sistem (proces sau nucleu) care gestionează thread-urile depinde de

modul de implementare a acestora.

Operaţiile de lucru cu thread-uri sunt furnizate cu ajutorul unor librării de programe (C, C++)

sau cu ajutorul unor apeluri sistem (în cazul sistemelor de operare: Windows NT, Sun

Solaris).

Esenţa conceptuală a threadului este aceea că execută o procedură sau o funcţie, în cadrul

aceluiaşi proces, concurent cu alte thread-uri. Contextul şi zonele de date ale procesului sunt

utilizate în comun de către toate thread-urile lui.

Esenţa de reprezentare în memorie a unui thread este faptul că singurul spaţiu de memorie ocupat

exclusiv este spaţiul de stivă. In plus, fiecare thread îşi întreţine propriul context, cu elemente

comune contextului procesului părinte al threadului.

In figura 2.5 sunt reprezentate trei thread-uri în cadrul aceluiaşi proces.

Există cazuri când se preferă folosirea proceselor în locul thread-urilor. De exemplu, când este

nevoie ca entităţile de execuţie să aibă identificatori diferiţi sau să-şi gestioneze independent

anumite atribute ale fişierelor (directorul curent, numărul maxim de fişiere deschise) [96].

PROCESUL P1

Context P1

PC

SP

Cod P1

Date P1

Heap P1

Stiva P1

PROCESUL P2

Context P2

PC

SP

Cod P2

Date P2

Heap P2

Stiva P2

Page 5: Programare Multi Threading

Programare multithreading - 5 –

Un program multi-thread poate să obţină o performanţă îmbunătăţită prin execuţia concurentă

şi/sau paralelă a thread-urilor. Execuţia concurentă a thread-urilor (sau pe scurt, concurenţă)

înseamnă că mai multe thread-uri sunt în progres, în acelaşi timp. Execuţia paralelă a thread-

urilor (sau pe scurt, paralelism) apare când mai multe thread-uri se execută simultan pe mai

multe procesoare.

Figura 2.3 Trei thread-uri într-un proces

2.2 Metodologia şi problematica utilizării şi sincronizării

threadurilor

In toate API-urile care operează cu threaduri (POSIX, Solaris, Windows, Java etc.) utilizatorul

trebuie să aibă în vedere următoarele aspecte:

Crearea unui thread.

Configurarea threadului creat.

Planificarea accesului la procesor.

Lansarea în execuţie.

Cooperarea cu alte threaduri ale programului şi / sau ale sistemului prin elemente de

sincronizare..

Terminarea activităţii unui thread.

In cele ce urmează vom prezenta cele mai importante elemente de sincronizare specifice

threadurilor.

2.2.1 Variabile mutex

Variabila mutex (mutual exclusion) este un instrument util pentru protejarea unor resurse

partajate, accesate concurent de mai multe thread-uri. Variabilele mutex sunt folosite, de

asemenea, pentru implementarea secţiunilor critice şi a monitoarelor (notiuni pe care le vom

defini în secţiunile imediat următoare).

PROCESUL P cu cu trei Threaduri

Context P

Context

thread 1

PC

SP

Context

thread 2

PC

SP

Context

thread 3

PC

SP

Cod P

Date P

Heap P

Stiva thread1

Stiva thread2

Stiva thread3

Page 6: Programare Multi Threading

Programare multithreading - 6 –

O variabilă mutex are două stări posibile: blocată (este proprietatea unui thread) sau neblocată

(nu este proprietatea nici unui thread). O variabilă mutex nu este proprietatea mai multor thread-

uri simultan. Un thread care vrea să obţină o variabilă mutex blocată de alt thread, trebuie să

aştepte până când primul o eliberează.

Operaţiile posibile asupra variabilelor mutex sunt: iniţializarea (static sau dinamic), blocarea

(pentru obţinerea accesului la resursa protejată), deblocarea (pentru eliberarea resursei protejate)

şi distrugerea variabilei mutex.

Din punct de vedere conceptual, o variabilă mutex este echivalentă cu un semafor s, care poate

lua două valori: 1 pentru starea neblocată şi 0 pentru starea blocată. (Un astfel de semafor se va

numi semafor binar). Operaţiile asupra unei variabile mutex m se definesc, cu ajutorul

semafoarelor, astfel:

Iniţializare: se defineşte un semafor m astfel încât v0(m) = 1.

Blocare: (după o eventuală deblocare de către alt thread): P(m).

Deblocare: V(m).

Distrugere: distrugerea semaforului m.

2.2.2 Variabile condiţionale

Variabile condiţionale sunt obiecte de sincronizare şi comunicare între thread-urile care aşteaptă

satisfacerea unei condiţii şi threadul care o realizează. O variabilă condiţională are asociate: un

predicat şi o variabilă mutex.

Predicatul dă condiţia ce trebuie să se realizeze şi care de obicei implică date partajate.

Variabila mutex asigură faptul că verificarea condiţiei şi intrarea în aşteptare, sau verificarea

condiţiei şi semnalarea îndeplinirii ei să fie executate ca şi operaţii atomice.

Operaţiile posibile asupra variabilelor condiţionale sunt:

Iniţializare: care poate fi statică sau dinamică.

Aşteptare (wait): threadul este pus în aşteptare până când i se va semnala din exterior

îndeplinirea condiţiei.

Semnalare (notify, broadcast, notifyall): threadul curent anunţă unul dintre thread-urile

ce aşteaptă îndeplinirea condiţiei, sau toate thread-urile ce aşteaptă îndeplinirea condiţiei.

Distrugere.

In capitolele următoare vom detalia utilizarea variabilelor mutex şi a variabilelor condiţionale pe

platformele Unix, Java şi Windows.

2.2.3 Conceptul de monitor

Conceptul de monitor a fost introdus de C.A.R. Hoare în 1974, [41]. Acolo, Hoare descrie

monitorul ca fiind un obiect folosit pentru realizarea execuţiei neconcurente a unui grup de

proceduri. Noţiunea de monitor combină paradigma programării orientate-obiect cu unele tehnici

de sincronizare.

In modelul lui Hoare, un monitor poate fi descris ca un obiect care conţine:

Page 7: Programare Multi Threading

Programare multithreading - 7 –

1. datele partajate

2. procedurile care accesează aceste date

3. o metodă de iniţializare a monitorului

Astfel, fiecare grup de proceduri este controlat de un monitor. În momentul rulării programului

multi-thread, monitorul permite unui singur thread să execute o procedură controlată de el. In

această situaţie, vom spune că threadul a ocupat monitorul. Dacă alte thread-uri invocă monitorul

în timp ce acesta este ocupat, ele sunt suspendate până când procedura monitor apelată de thread-

ul respectiv îşi încheie activitatea, ceea ce coincide cu eliberarea monitorului de către thread.

Primele implementări ale conceptului de monitor au fost realizate în limbajele Pascal Concurent

şi Mesa. Limbajul Modula foloseşte de asemenea acest concept. O variantă a conceptului de

monitor a fost implementată în limbajul Java cu ajutorului modificatorului synchronised, aşa

cum vom vedea în capitolele următoare.

Monitorul este un concept mai uşor de manevrat decât semaforul, motiv pentru care este mai

utilizat în limbajele de programare specifice. Totuşi, din punct de vedere conceptual, un monitor

poate fi descris simplu folosind un singur semafor binar, cu valoarea iniţială 1. Fiecare procedură

a monitorului începe cu P(s) şi se încheie cu V(s):

var semaphore s = 1;

- - - - - - - - - -

Pentru fiecare procedura a monitorului:

P(s)

codul corpului procedurii

V(s)

2.3 Relaţii thread – program – sistem de operare

2.3.1 Contextul şi stările unui thread

Un thread există în cadrul unui proces. El este compus dintr-un context cu atribute specifice, o

structură utilizator, o stivă, o zonă de date privată şi un set de instrucţiuni care se vor executa.

Prin intermediul atributelor unui thread se pot defini caracteristici specifice acestuia.

Principalele atribute ale unui thread sunt: identificatorul / numele threadului, politica de

planificare, prioritatea, faptul că fie direct nucleul sistemului de operare, fie direct procesul

utilizator controlează nemijlocit execuţia threadului.

Structura utilizator conţine copii ale valorilor regiştrilor generali: PC - Program Counter, SP -

Stack Pointer etc. Fiecare thread are stiva sa proprie şi o zonă privată de date, similară zonei

heap a unui proces.

Stările unui thread şi tranziţiile dintre acestea sunt într-o oarecare măsură similare cu cele ale

proceselor [13]. In diverse lucrări, aceste stări sunt numite diferit, dar funcţionalităţile sunt

aceleaşi. Implementările actuale, thread-urile se pot găsi într-una din următoarele stări:

Created - Threadul este creat, respectiv obiectul thread a fost construit.

Ready (Runnable) - indică situaţia în care threadul este gata de execuţie şi se află într-o coadă

de aşteptare în vederea execuţiei.

Run (Running) - indică faptul că threadul este efectiv în execuţie. In cazul thread-urilor,

execuţia se poate face fie în mod nucleu, fie în mod user (vom reveni cu precizări detaliate).

Page 8: Programare Multi Threading

Programare multithreading - 8 –

Wait (Sleeping, asleep, blocked) - indică situaţia în care threadul se află fie blocat în

aşteptarea producerii unui eveniment, fie aşteaptă scurgerea unui interval de timp, fie

aşteaptă terminarea unei operaţii I/O.

Suspend (Stopped) - atunci când threadul însuşi sau un alt thread a comandat suspendarea pe

moment a execuţiei. Aceasta urmează să fie continuată (resume) la iniţiativa unui alt thread.

Terminated - atunci când threadul îşi încheie definitiv execuţia.

Figura 4.1 ilustrează aceste stări şi evenimentele care le generează.

Execuţia unui thread este coordonată de o anumită funcţie sau de o metodă de control specifică.

Este posibil ca aceeaşi funcţie să coordoneze simultan mai multe thread-uri.

Păstrând analogia thread-uri - procese, se poate constata că:

Un thread este o entitate individuală planificabilă.

Thread-urile pot fi întrerupte preemptiv. Astfel, un thread nu poate presupune că accesul său

la o variabilă este neinteruptibil, decât dacă furnizează mecanisme explicite de blocare.

Thread-urile pot beneficia fie de o concurenţă reală, fie de o concurenţă logică, în funcţie de

numărul procesoarelor disponibile în sistem.

Figura 2.4 Stările unui thread

Fiecare thread are acces la următoarele resurse ale procesului părinte:

întreg spaţiu de adrese al procesului.

resurse întreţinute de sistemul de operare: directorul curent, descriptorii de fişiere,

fişierele / înregistrările blocate în mod curent, drepturile de acces la fişiere sau la

facilităţile IPC.

Thread-urile sunt entităţi potrivite pentru programarea modulară, în care activităţile distincte din

program au asociate entităţi de execuţie (în acest caz, thread-uri). Acestea partajează resurse şi îşi

desfăşoară activitatea independent sau cu ajutorul unor mecanisme de comunicare şi

sincronizare. Printre avantajele thread-urilor faţă de procese amintim faptul că operaţiile de

creare, planificare şi comunicare sunt mai puţin costisitoare, deoarece thread-urile partajează

spaţiul de adrese al procesului curent.

2.3.2 Tipuri de thread-uri

2.3.2.1 O clasificare a thread-urilor

Runnable

Running

user modeCreated Terminated

Stopped

Running kernel mode

Sleeping

creare

thread

continuare

return apel sistem

iesire

blocare/ sleep

trezire

schimbare

context

stop

intrerupere

Page 9: Programare Multi Threading

Programare multithreading - 9 –

Entitatea thread poate fi implementată fie direct de către sistemul de operare, fie cu ajutorul unei

biblioteci speializate, la nivelul programului utilizator. In funcţie de soluţia adoptată, thread-urile

se împart în trei categorii:

thread-uri utilizator (user, program): implementate cu ajutorul unei biblioteci specializate.

thread-uri nucleu (kernel, sistem): implementate folosind apeluri sistem specializate.

utilizări combinate: thread-uri utilizator care sunt create prin program şi apoi multiplexate pe

thread-uri sistem.

Această clasificare este dependentă de platforma sub care se lucrează. Fiecare implementare

adoptă implicit una sau mai multe dintre categoriile de mai sus. Iată câteva astfel de

implementări:

Biblioteca pthreads permite crearea thread-urilor utilizator sub sisteme Unix.

Sub Linux, pentru crearea thread-urilor sistem este folosit apelul sistem _clone().

Sub Solaris, se pot utiliza combinaţii de thread-uri sistem – utilizator, cu ajutorul lwp-urilor –

Light Weight Processes-, care multiplexează mai multe thread-uri utilizator pe un singur

thread sistem.

Platformele Microsoft utilizează implicit thread-uri nucleu. Pentru crearea de thread-uri user

se utilizează conceptul de fibră (fiber).

Platformele Java numesc thread-uri native pe cele sistem (executate de sistemul de operare,

nu de către JVM) respectiv thread-uri green, cele executate în interiorul JVM.

In figura 4.2 este prezentată relaţia dintre thread-uri şi lwp-uri pe Solaris:

Figura 2.5 Relaţia dintre thread-uri şi lwp-uri pe Solaris

In figura 4.3 este prezentată relaţia dintre thread-uri şi fibre pe Windows NT:

Figura 2.6 Relaţia dintre thread-uri şi fibre pe Windows NT

2.3.2.2 Thread-uri nucleu

Date globale

Cod aplicatie

Fibre

Thread-uri

Structura Proces

Nucleu Windows NT

Cod aplicatie,

Date globale

Biblioteca de thread-uriThread-uri

LWP-uri

Structura Proces

Nucleu Solaris

Page 10: Programare Multi Threading

Programare multithreading - 10 –

Thread-urile nucleu sunt create prin apeluri sistem specifice şi sunt vizibile în nucleul sistemului

de operare. Acesta menţine un context (valori din regiştri, inclusiv PC) şi asigură gestiunea

(crearea, planificarea, distrugerea) acestor thread-uri.

Thread-urile nucleu consumă mai multe resurse, deoarece presupun gestiunea unor structuri de

date în nucleu şi sunt planificate tot în nucleu, după o metodă preemptivă. Toate serviciile de

care aplicaţia are nevoie trebuie să fie incluse în nucleu. Pe lângă consumul de resurse, un alt

dezavantaj este că execuţia este puţin încetinită, datorită faptului că schimbarea contextului se

face cu ajutorul nucleului.

Sub Linux, implementarea thread-urilor Posix are la bază apelul sistem _clone(). Astfel,

aceste thread-uri sunt create în spaţiu utilizator, după care sunt multiplexate pe câte un thread

sistem. Planificarea este realizată în nucleu. Principalul avantaj al acestei abordări este

posibilitatea utilizării sistemelor multiprocesor şi implementarea unei biblioteci mai robuste în

special în ceea ce priveşte apelurile sistem de blocare.

Printre avantajele thread-urilor sistem amintim:

nucleul poate planifica simultan mai multe thread-uri din acelaşi proces, să ruleze pe

procesoare diferite.

rutinele utilizate direct de nucleu pot fi multi-thread.

2.3.2.3 Thread-uri utilizator

Thread-urile utilizator sunt create de aplicaţie, iar sistemul de operare nu are cunoştinţă de

existenţa lor. In acest caz, serviciile nucleu pot fi folosite doar de către procese sau de către

thread-urile sistem. Deşi nucleul nu are cunoştinţă de thread-urile utilizator, el continuă să

gestioneaze activitatea proceselor.

Thread-urile utilizator pot fi create, terminate şi sincronizate folosind API-uri oferite de

biblioteca de thread-uri. Ele există în spaţiul de adrese utilizator şi execută codul utilizator.

Aceste thread-uri sunt planificate cooperativ [13]. Fiecare thread cedează resursa procesor printr-

o întrerupere explicită sau printr-un semnal. Când un thread invocă un apel sistem, întregul

proces va fi blocat până la terminarea apelului. Stările thread-urilor sunt independente de stările

procesului. Astfel, pentru biblioteca de thread-uri, threadul respectiv va fi tot în starea run, deşi

procesul este blocat.

Thread-urile utilizator nu pot beneficia de avantajele unui sistem multiprocesor, decât dacă sunt

multiplexate automat pe thread-uri sistem. Altfel, ele NU sunt recomandate pentru aplicaţiile

care interacţionează frecvent cu sistemul de operare, cum ar fi de exemplu o aplicaţie distribuită

sau un server de procesare.

Printre avantajele thread-urilor utilizator amintim:

planificarea este independentă de sistemul de operare şi poate fi definită în funcţie de

cerinţele aplicaţiei.

schimbarea contextului threadului runnable nu implică nucleul.

thread-urile utilizator pot rula pe orice sistem de operare, care dispune de biblioteca

respectivă.

Page 11: Programare Multi Threading

Programare multithreading - 11 –

2.4 Exemple de probleme rezolvabile prin thread-uri

2.4.1 Adunarea în paralel a n numere

Vom da, ca prim exemplu de utilizare a thread-urilor, evaluarea în paralel a sumei mai multor

numere întregi. Evident, operaţia de adunare a n numere, chiar dacă n este relativ mare, nu

impune cu necesitate însumarea lor în paralel. O facem totuşi pentru că reprezintă un exemplu

elocvent de calcul paralel, în care esenţa este reprezentată de organizarea prelucrării paralele,

aceeaşi şi pentru calcule mult mai complicate.

Presupunem că se dă un număr natural n şi un vector a având componentele întregi a[0], a[1], .

. . a[n-1]. Ne propunem să calculăm, folosind cât mai multe thread-uri, deci un paralelism cât

mai consistent, suma acestor numere. Modelul de paralelism pe care ni-l propunem este ilustrat

mai jos, pentru m = 8:

Mai întâi sunt calculate, în paralel, următoarele patru adunări:

a[0] = a[0] + a[1]; a[2] = a[2] + a[3]; a[4] = a[4] + a[5]; a[6] = a[6] + a[7];

După ce primele două adunări, respectiv ultimele două adunări s-au terminat, se mai execută în

paralel încă două adunări:

a[0] = a[0] + a[2]; a[4] = a[4] + a[6];

In sfârşit, la terminarea acestora, se va executa:

a[0] = a[0] + a[4];

Operaţiile de adunare se desfăşoară în paralel, având grijă ca fiecare adunare să se efectueze

numai după ce operanzii au primit deja valori în adunările care trebuie să se desfăşoare înaintea

celei curente. Este deci necesară o operaţie de sincronizare între iteraţii.

Considerând că fiecare operaţie de adunare se execută într-o unitate de timp, din cauza

paralelismului s-au consumat doar 3 unităţi de timp în loc de 7 unităţi de timp care s-ar fi

consumat în abordarea secvenţială. In calcule s-au folosit 7 thread-uri, din care maximum 4 s-au

executat în paralel.

Să considerăm acum problema pentru n numere şi să implementăm soluţia, cu intenţia de a folosi

un număr mxim de thread-uri.

Mai întâi extindem setul de numere până la m elemente, unde m este cea mai mică putere l a lui 2

mai mare sau egală cu n, adică:

2l-1

< n <= 2l = m, unde l = partea întreagă superioară a lui log2n

Elementele a[n], . . ., a[m-1] vor primi valoarea 0.

In ipoteza că o adunare durează o unitate de timp, soluţia pe care o propunem va utiliza m-1 =

2l-1 thread-uri, iar adunarea tuturor numerelor va dura l unităţi de timp. Pentru aceasta vom

adopta o schemă arborescentă de numerotare a thread-urilor, ilustrată în figura 4.5 pentru 32 de

numere.

Page 12: Programare Multi Threading

Programare multithreading - 12 –

Pentru adunare sunt necesare m-1 thread-uri, organizate pe l+1 nivele. Numerotăm thread-urile

cu 1, 2, . . ., m-1, iar nivelele cu 0, 1, . . ., l. Este uşor de văzut că în schema arborescentă de

numerotere nodurile interioare sunt thread-uri, iar frunzele sunt operanzii adunării.

Fiecare nivel k conţine 2k noduri. Fiecare nod interior de pe un nivel k are 2

l-k frunze

descendente. Dacă i este un thread, atunci:

nivel(i) = k cu proprietatea: 2k <= i < 2

k+1 , rezultă k = [log2i]

Sarcina threadului i este de a însuma cele 2l-k

elemente care-i sunt descendente, începând cu

elementul de pe poziţia 2l-k

(i-2k).

Figura 2.7 Schemă arborescentă de adunare paralelă a 32 de numere

Dacă nivel(i) < l-1, atunci:

aşteaptă terminarea thread-urilor fii cu numerele 2i şi 2i+1;

adună a[2l-k

(i-2k)] cu a[2

l-k-1((2i+1)-2

k+1)] şi depune rezultatul în a[2

l-k(i-2

k)]

Dacă nivel(i) = l-1, atunci:

adună a[2(i-2l-1

)] cu a[2(i+2l-1

)+1] şi depune rezultatul în a[2(i-2l-1

)]

Această problemă va fi rezolvată pe toate cele trei platforme pe care le avem în vedere: Unix,

Windows şi Java. Deoarece sursele programelor vor fi suficient de asemănătoare, prezentăm în

această secţiune secvenţele comune de program. Valoarea lui n este preluată de la linia de

comandă. Valorile lui m şi ale lui l se calculează cu instrucţiunea:

for (l=0, m=1; n>m; l++, m*=2);

Pentru a se putea urmări mai uşor, valorile celor n numere de adunat din vectorul a vor fi toate 1.

Firesc, poziţiile de la n la m-1 vor fi făcute 0. Avem deci instrucţiunile:

for (i = 0; i < n; i++) a[i] = 1;

1

2

4 5

8 9

3

6 7

1110 1312 1514

1716 18 19 2120 22 23 24 25 26 27 2928 30 31

a0

a1

a2

a3

a4

a5

a6

a7

a8

a9

a10

a11

a12

a13

a14

a15

a16

a17 a19

a18 a20

a21

a22

a23

a24

a25

a26

a27

a28

a29

a30

a31

Page 13: Programare Multi Threading

Programare multithreading - 13 –

for (i = n; i < m; i++) a[i] = 0;

Fiecare thread care va efectua o adunare este caracterizat de următorii şapte parametri:

i este numărul threadului, în conformitate cu schema de numerotare de mai sus.

sa şi da sunt indicii stâng şi drept ai numerelor din tabloul a care trebuie adunate de către

threadul curent.

astept este un boolean cu valoarea true dacă threadul curent trebuie să aştepte după thread-

uri subalterni.

st şi dt sunt numerele thread-urilor stâng şi drept subalterni direcţi ai threadului curent.

Dacă ei au valoarea 0, înseamnă că threadul curent nu are subalterni.

status indică starea threadului: -1 din momentul creării până în momentul lansării lui în

execuţie şi 0 după lansarea în execuţie. Este necesar deoarece nu se poate comanda aşteptarea

terminării unui thread decât dacă acesta este în execuţie!

Variabila dk reţine de fiecare dată valoarea 2k, iar variabila dlmk reţine valoarea 2

l-k, valori

calculate din aproape în aproape prin înmulţiri / Impărţiri cu 2. Determinarea primilor şase

parametri necesari creării thread-urilor se face cu secvenţa de instrucţiuni:

for (k = 0, i = 1, n = 1, dk = 1, dlmk = m;

k < l;

k++, dk *= 2, dlmk /= 2, n *= 2)

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

In acest moment, cei şase parametri sunt, în ordine:

i, 2*i, 2*i+1, k<l-1, dlmk*(i-dk), dlmk/2*(2*i+1-dk*2)

Fiecare thread va afişa numerele thread-urilor după care aşteaptă, numărul threadului curent şi

operaţia executată. De exemplu,

Dupa 10 si 11: T5: a[4]+a[6] => a[4]

Inseamnă că threadul 5 aşteaptă după thread-urile 10 şi 11, după care face adunarea lui a[4] cu

a[6] şi pune rezultatul în a[4].

2.4.2 Problema producătorului şi consumatorului

Se va simula problema clasică 2.7.1. Se dă un Recipient având o capacitate limitată MAX.

Există un număr oarecare de procese numite Producător, care depun, în ordine şi ritm aleator,

numere întregi consecutive în acest recipient. Mai există un număr oarecare de procese

Consumator, care extrag pe rând câte un număr dintre cele existente în recipient.

In textele sursă, tablourile p, v şi metoda / funcţia scrie, sunt folosite pentru afişarea stării

recipientului la fiecare solicitare a uneia dintre get sau put. Numărul de producători şi de

consumatori sunt fixaţi cu ajutorul constantelor P şi C.

In sursa unui thread producător, variabila art dă numărul elementului produs, iar i este numărul

threadului. După efectuarea unei operaţii put, threadul face sleep un interval aleator de timp.

In sursa unui thread consumator, după o operaţie get, acesta intră în sleep un interval aleator

de timp.

Page 14: Programare Multi Threading

Programare multithreading - 14 –

Situaţia la un moment dat este dată prin stările producătorilor, stările consumatorilor şi

conţinutul bufferului după efectuarea operaţiei.

Stările fiecărui producător (P) sunt afişate prin câte un întreg:

- <0 indică aşteptare la tampon plin pentru depunerea elementului pozitiv corespunzător,

- >0 dă valoarea elementului depus,

- 0 indică producător inactiv pe moment.

Stările fiecărui consumator(C) sunt afişate prin câte un întreg:

- -1 indică aşteptare la tampon gol,

- >0 dă valoarea elementului consumat,

- 0 indică consumator inactiv pe moment.

In linia de ieşire de mai jos se indică un buffer plin şi doi producători care aşteaptă:

P0d75 P1d0 P2d0 P3d-76 P4d-77 C0s0 C1s0 C2s0 C3s0 C4s0 B: 66 67 68 69 70 71

72 73 74 75

Inseamnă că producătorii 0, 1 şi 2 sunt inactivi, producătorul 3 doreşte să depună numărul 76,

producătorul 4 doreşte să depună numărul 77, în timp ce consumatorii de asemenea sunt inactivi.

Aceasta se întâmplă deoarece bufferul este plin (capacitatea lui este 10) şi conţine numerele 66-

75. (Situaţia se va schimba când va apare un consumator).

Buffer gol şi se aşteaptă consumatori:

P0d0 P1d0 P2d0 P3d0 P4d0 C0s0 C1s6 C2s0 C3s0 C4s-1 B:

P0d0 P1d0 P2d0 P3d0 P4d0 C0s0 C1s0 C2s-1 C3s-1 C4s7 B:

Situaţii "de mijloc" cu buffer pe jumătate plin şi cu producători care asteaptă:

P0d0 P1d0 P2d0 P3d268 P4d0 C0s0 C1s0 C2s0 C3s0 C4s0 B: 268

P0d0 P1d0 P2d0 P3d269 P4d0 C0s0 C1s0 C2s0 C3s0 C4s0 B: 268 269

In buffer nu sunt puse neapărat valori consecutive:

P0d0 P1d0 P2d0 P3d0 P4d0 C0s0 C1s0 C2s0 C3s0 C4s105 B: 106 107 108 109 111

114 113 110 112

P0d0 P1d0 P2d0 P3d0 P4d0 C0s0 C1s0 C2s106 C3s0 C4s0 B: 107 108 109 111 114

113 110 112

2.4.3 Problema cititorilor şi a scriitorilor

Problema este de asemenea prezentată în 2.7.2. Simularea noastră se face astfel.

Se dă un obiect pe care Il vom numi “bază de date” (Bd), . Există un număr oarecare de procese

numite Scriitor, care efectuează, în ordine şi ritm aleator, scrieri în bază. Mai există un

număr oarecare de procese Cititor, care efectuează citiri din Bd.

O operaţie de scriere este efectuată asupra Bd în mod individual, fără ca alţi scriitori sau cititori

să acceseze Bd în acest timp. Dacă Bd este utilizată de către alte procese, scriitorul aşteaptă până

când se eliberează, după care execută scrierea.

Page 15: Programare Multi Threading

Programare multithreading - 15 –

In schimb, citirea poate fi efectuată simultan de către oricâţi cititori, dacă nu se execută nici o

scriere în acel timp. In cazul că asupra Bd se execută o scriere, cititorii aşteaptă până când se

eliberează Bd.

In situaţia solicitării simultane de către cititori şi cititori, au prioritate procesele scriitor.

Esenţa programului constă din definirea variabilei cititori şi a metodelor citeste şi

scrie.

Variabila cititori reţine de fiecare dată câţi cititori sunt activi la un moment dat. După cum

se poate observa, instanţa curentă a lui Bd este blocată (pusă în regim de monitor) pe parcursul

acţiunilor asupra variabilei cititori. Aceste acţiuni sunt efectuate numai în interiorul

metodelor scrie şi citeste.

Metoda citeste incrementează (în regim monitor) numărul de cititori. Apoi, posibil concurent

cu alţi cititori, îşi efectuează activitatea, care aici constă doar în afişarea stării curente. La

terminarea acestei activităţi, în regim monitor decrementează şi anunţă thread-urile de aşteptare.

Acestea din urmă sunt cu siguranţă numai scriitori.

Metoda scrie este atomică (regim monitor), deoarece întreaga ei activitate se desfăşoară fără

ca celelalte procese să acţioneze asupra Bd.

Metoda afisare are rolul de a afişa pe ieşirea standard starea de fapt la un moment dat.

Situaţia la un moment dat este dată prin stările cititorilor şi ale scriitorilor. Stările fiecărui scriitor

(S) sunt afişate prin câte un întreg: -1 indică aşteptare ca cititorii să-şi termine operaţiile, 1 indică

scriere efectivă, iar 0 indică scriitor inactiv pe moment. In mod analog, stările fiecărui cititor (C)

sunt afişate prin câte un întreg: -1 indică aşteptarea terminării scrierilor, 1 indică citire efectivă,

iar 0 indică cititor inactiv pe moment.

Mai jos sunt date câteva situaţii ivite în cursul execuţiei, la execuţii diferite se observă

conţinuturi diferite ale buferului.

Situaţie echilibrată privind succesiunea la Bd: S trebuie să aştepte toţi C înainte de a putea

scrie şi are prioritate maximă faţă de C care are prioritate minimă:

S0s0 S1s1 S2s0 S3s0 S4s0 C0c0 C1c0 C2c0 C3c0 C4c0

S0s0 S1s0 S2s0 S3s0 S4s0 C0c1 C1c0 C2c0 C3c0 C4c0

S0s0 S1s0 S2s0 S3s0 S4s1 C0c0 C1c0 C2c0 C3c0 C4c0

S0s0 S1s0 S2s0 S3s0 S4s0 C0c0 C1c1 C2c0 C3c0 C4c0

Un S aşteaptă după alt S

S0s-1 S1s1 S2s0 S3s-1 S4s0 C0c0 C1c0 C2c0 C3c0 C4c0

S0s-1 S1s0 S2s1 S3s-1 S4s0 C0c0 C1c0 C2c0 C3c0 C4c0

S0s-1 S1s0 S2s0 S3s-1 S4s1 C0c0 C1c0 C2c0 C3c0 C4c0

Sau S aşteaptă după alţi C

S0s-1 S1s0 S2s0 S3s0 S4s0 C0c0 C1c0 C2c0 C3c0 C4c1

Un C nu aşteaptă după alţi C; pot exista mai mulţi C la un moment dat, in timp ce toţi S

aşteaptă

S0s-1 S1s-1 S2s-1 S3s-1 S4s-1 C0c1 C1c1 C2c1 C3c1 C4c0

Page 16: Programare Multi Threading

Programare multithreading - 16 –

S0s-1 S1s-1 S2s-1 S3s-1 S4s-1 C0c1 C1c1 C2c1 C3c1 C4c0

S0s-1 S1s-1 S2s-1 S3s-1 S4s-1 C0c1 C1c1 C2c0 C3c1 C4c0

Se poate vedea uşor că la număr mai mare de scriitori decât cititori, sunt mai multi scriitori care

aşteaptă după cititori. La număr mai mare de cititori decât scriitori, sunt mai mulţi cititori care

aşteaptă după un scriitor.

2.5 Thread-uri pe platforme Unix: Posix şi Solaris

2.5.1 Caracteristici şi comparaţii Posix şi Solaris

Thread-urile Posix sunt răspândite pe toate platformele Unix, inclusiv Linux, SCO, AIX, Xenix,

Solaris, etc. In particular, unele dintre aceste platforme au şi implementări proprii, cu

caracteristici mai mult sau mai puţin apropiate de Posix. Dintre acestea, implementarea proprie

platformei Solaris este cea mai elaborată şi mai răspândită, motiv pentru care o vom trata în

această secţiune, în paralel cu implementarea Posix. Cele două modele au multe similarităţi la

nivel sintactic dar au modalităţi de implementare diferite

2.5.1.1 Contextul şi stările unui thread

Contextul general, prezentat în 4.1.1, se particularizează în cazul thread-urilor Posix şi Solaris.

Astfel, fiecare thread este reprezentat printr-o structură care conţine:

Identificatorul threadului, asociat la crearea acestuia. El este folosit ca index într-o tabelă cu

pointeri la structurile threadului.

O zonă de memorie folosită pentru a salva contextul de execuţie al threadului curent, când se

dă controlul altui thread.

Prioritatea threadului.

Pointer la stiva threadului. Zona de memorie pentru stivă este alocată automat de biblioteca

de lucru cu thread-uri sau prin program.

Stările unui thread. Orice entitate de tip thread (thread, mutex, variabilă condiţională, etc.) poate

fi creată în două stări: detached sau nondetached. Starea detached specifică faptul că structurile

interne ale threadului sunt marcate pentru ştergere, iar în momentul în care threadul îşi încheie

activitatea el este radiat din sistem şi memoria pe care o ocupă este eliberată. Starea nondetached

păstrează structurile interne, permiţând astfel reactivarea threadului.

După creare, stările unui thread rămân valabile cele descrise în capitolul 4.1.1.

2.5.1.2 Caracteristici ale thread-urilor sub Linux

Implementarea thread-urilor sub Linux respectă standardul Posix şi se realizează conform

modelului multithreading 1x1. Astfel, fiecare thread creat în spaţiu utilizator are asociat un

thread vizibil nucleului.

Implementarea thread-urilor sub Linux se conformează filozofiei de bază a Linux-ului, aceea de

a optimiza în primul rând nucleul, iar definirea structurilor de date din sistem şi implementarea

operaţiilor să beneficieze de această optimizare.

In ceea ce priveşte planficarea, sistemul alege următorul thread care se va executa în funcţie de

politica de planificare şi prioritatea thread-urilor. Aceste proprietăţi sunt reţinute de obiectele

purtătoare de atribute, prin apeluri specifice, după cum vom vedea în secţiunile viitoare.

Page 17: Programare Multi Threading

Programare multithreading - 17 –

Avantajele implementării thread-urilor sistem sub Linux includ: simplitatea proiectării

programului, cod redundant puţin, optimizarea operaţiilor inter-proces, flexibilitate semantică

globală, o planificare bună a thread-urilor.

Principalul dezavantaj este aspectul monolitic al programului, care reduce flexibilitatea la nivel

utilizator.

2.5.1.3 Caracteristici ale thread-urilor sub Solaris

Thread-urile Solaris fac parte din categoria thread-urilor hibride şi sunt implementate folosind

modelul multithreading MxN. Thread-urile create în spaţiu utilizator (thread-uri user) sunt

multiplexate printre lwp-urile (thread-uri nucleu) disponibile pentru procesul curent. Un lwp

poate să ruleze, la un moment dat, un singur thread. După un timp, lwp-ul va “abandona”

threadul pe care Il execută şi alege altul. Threadul abandonat va fi reluat spre a fi executat, nu

neaparat de acelaşi lwp.

Pe sistemele multiprocesor, execuţia concurentă a mai multor lwp-uri implică o concurenţă reală

a procesului. Un thread liber din starea ready este executat doar în momentul în care există un

lwp disponibil care să-l preia. In anumite cazuri, de exemplu când threadul trebuie să răspundă la

un semnal, această latenţă în execuţie poate conduce la rezultate inadecvate. Evitarea acestei

situaţii se face legând threadul de un lwp. Când threadul se termină, este eliminat şi lwp-ul

asociat.

Un lwp devine disponibil atunci când threadul asociat (legat) se termină, când threadul este

blocat printr-un mecansim de sinronizare, sau când threadul cedează controlul altui thread.

Planificarea thread-urilor multiplexate printre lwp-uri se realizează pe două nivele:

Planificare la nivel de bibliotecă: planificatorul asociază thread-uri multiplexate pe diferite

lwp-uri pentru a fi executate, apoi prin rotaţie le suspendă temporar, pentru a da controlul

altor thread-uri. In alegerea thread-urilor care se vor executa, planificatorul ţine cont de

nivelul de prioritate a acestora, cele cu prioritate mai mare vor fi executate înaintea celor cu

prioritate mai mică, iar la priorităţi egale se aplică disciplina FIFO.

planificare la nivel de sistem: nucleul sistemului asociază lwp-uri la procesoare, apoi prin

rotaţie le suspendă temporar, pentru a da controlul altor lwp-uri

Planificatorul de bibliotecă şi planificatorul sistem funcţionează independent dar, global pe

proces, efectele lor interacţionează. Planificatorul de bibliotecă asociază un thread la un lwp, dar

nu poate spune când se va executa acest lwp.

Nucleul nu are cunoştinţă de faptul că biblioteca utilizează lwp-uri ca să implementeze thread-uri

utilizator. El îşi întreţine propriul context de planificare, care este diferit de contextele de

planificare la nivel de bibliotecă.

Thread-urile active sunt menţinute într-o coadă. Dacă un thread din starea run are o prioritate

mai mare decât a unui thread activ, acel thread activ este pus temporar în aşteptare şi eliminat din

lwp-ul asociat. lwp-ul va rula noul thread cu prioritate mai mare.

2.5.1.4 Similarităţi şi facilităţi specifice

Intre thread-urile Posix şi Solaris există un grad Posix mare de similaritate, atât la nivel sintactic,

cât şi funcţional. Pentru operaţiile principale cu entităţile thread există apeluri diferite pentru

Posix şi Solaris. Funcţiile thread-urilor Posix au prefixul pthread_ iar cele Solaris thr_.

Page 18: Programare Multi Threading

Programare multithreading - 18 –

Astfel, o conversie a unui program simplu cu thread-uri de pe una din cele două platforme pe

cealaltă, se realizează doar la nivel sintactic, modificând numele funcţiei şi a unor parametri.

Inainte de a detalia principalele operaţii cu thread-uri Posix şi Solaris, punctăm câteva diferenţe

Intre aceste platforme.

Facilităţi Posix care nu sunt prezente pe Solaris:

portabilitate totală pe platforme ce suportă Posix

obiecte purtătoare de atribute

conceptul de abandon (cancellation)

politici de securitate

Facilităţi Solaris care nu sunt prezente pe Posix:

blocări reader/writer

posibilitatea de a crea thread-uri daemon

suspendarea şi continuarea unui thread

setarea nivelului de concurenţă (crearea de noi lwp-uri)

2.5.2 Operaţii asupra thread-urilor: creare, terminare

Inainte de a descrie operaţiile cu thread-uri, trebuie să precizăm că pentru thread-urile Posix

trebuie folosit fişierul header <pthread.h>, iar pentru Solaris headerele <sched.h> şi

<thread.h>. Compilările în cele două variante se fac cu opţiunile: -lpthread (pentru a

indica faptul că se foloseşte biblioteca libpthread.so.0, în cazul thread-urilor Posix),

respectiv opţiunea –lthread, care link-editează programul curent cu biblioteca

libthread.so, în cazul thread-urilor Solaris.

2.5.2.1 Crearea unui thread

Posix API: int pthread_create(pthread_t *tid, pthread_attr_t *attr,

void *(*func)(void*), void *arg);

Solaris API: int thr_create(void *stkaddr, size_t stksize, void *(*func)(void*),

void *arg, long flags, thread_t *tid);

Prin aceste funcţii se creează un thread în procesul curent şi se depune în pointerul tid

descriptorul threadului. Funcţiile întorc valoarea 0 la succes şi o valoare nenulă (cod de eroare),

în caz de eşec.

Execuţia noului thread este descrisă de funcţia al cărei nume este precizat prin parametrul func.

Această funcţie are un singur argument de tip pointer, transmis prin argumentul arg.

Varianta Posix prevede atributele threadului prin argumentul attr. Asupra acestuia vom reveni

Intr-o secţiune ulterioară. Pe moment vom folosi pentru attr valoarea NULL, prin aceasta

indicând stabilirea de atribute implicite de către sistem.

stkaddr şi stksize indică adresa şi lungimea stivei threadului. Valorile NULL şi 0 pentru

aceşti parametri cer sistemului să fixeze valori implicite pentru adresa şi dimensiunea stivei.

Page 19: Programare Multi Threading

Programare multithreading - 19 –

Parametrul flags -reprezintă o combinaţie de constante legate prin ‘|’, cu valori specifice

thread-urilor Solaris:

THR_SUSPENDED – determină crearea threadului în starea suspendat pentru a permite

modificarea unor atribute de planificare înainte de execuţia funcţiei ataşată threadului. Pentru

a începe execuţia threadului se apelează thr_continue.

THR_BOUND –leagă threadul de un nou lwp creat în acest scop. Threadul va fi planificat

doar pe acest lwp.

THR_DETACHED – creează un nou thread în starea detached. Resursele unui thread

detached sunt eliberate imediat la terminarea threadului. Pentru acest thread nu se poate apela

thr_join şi nici nu se poate obţine codul de ieşire.

THR_INCR_CONC sau, echivalent THR_NEW_lwp – incrementează nivelul de concurenţă

prin adăugarea unui lwp la colecţia iniţială, indiferent de celelalte flag-uri.

THR_DAEMON – creează un nou thread cu statut de daemon (de exemplu, pentru a trata

evenimente asincrone de I/O). Procesul de bază se termină când ultimul thread non-daemon

îşi încheie execuţia.

Dacă sunt precizate atât THR_BOUND cât şi THR_INCR_CONC sunt create două lwp-uri

odată cu crearea threadului.

2.5.2.2 Terminarea unui thread şi aşteptarea terminării lui

In mod obişnuit, terminarea unui thread are loc atunci când se termină funcţia care descrie

threadul. Din corpul acestei funcţii se poate comanda terminarea threadului prin apelurile

funcţiilor pthread_exit, respectiv thr_exit, cu prototipurile:

Posix API: int pthread_exit(int *status);

Solaris API: int thr_exit(int *status);

Pointerul status se foloseşte atunci când se doreşte ca în urma execuţiei threadul să întoarcă

nişte date rezultat spre procesul părinte al threadului. După cum vom vedea imediat, acesta

obţine datele printr-un apel de tip join. In cele mai multe cazuri, status are valoarea NULL.

Un apel pthread_exit/thr_exit termină procesul, numai dacă threadul apelant este

ultimul thread non-daemon din proces.

La terminarea unui thread, acesta este pus într-o listă deathrow. Din motive de performanţă,

resursele asociate unui thread nu sunt eliberate imediat ce acesta îşi încheie execuţia. Un thread

special, numit reaper parcurge periodic lista deathrow şi dealocă resursele thread-urilor

terminate [30].

Posix permite comandarea terminării - abandonului - unui thread de către un alt thread. Trebuie

remarcat că, în cazul utilizării abandonului, pot să apară probleme dacă execuţia threadului este

întreruptă în interiorul unei secţiuni critice. De asemenea, threadul nu va fi abandonat înainte de

a dealoca resurse precum segmente de memorie partajată sau descriptori de fişiere.

Pentru a depăsi problemele de mai sus, interfaţa cancellation permite abandonarea execuţiei

threadului, doar în anumite puncte. Aceste puncte, numite puncte de abandon, pot fi stabilite prin

apeluri specifice.

Abandonarea threadului se poate realiza în 3 moduri:

Page 20: Programare Multi Threading

Programare multithreading - 20 –

i) asincron, ii) în diferite puncte, în timpul execuţiei, conform specificaţiilor de implementare a

acestei facilităţi sau iii) în puncte discrete specificate de aplicaţie.

Pentru efectuarea abandonului se apelează:

int pthread_cancel(pthread_t tid);

cu identificatorul threadului ca argument.

Cererea de abandon este tratată în funcţie de starea threadului. Această stare se poate seta

folosind apelurile: pthread_setstate() şi pthread_setcanceltype().

Funcţia pthread_setcancelstate stabileşte punctul respectiv, din threadul curent, ca

punct de abandon, pentru valoarea PTHREAD_CANCEL_ENABLE şi interzice starea de

abandon, pentru valoarea PTHREAD_CANCEL_DISABLE.

Funcţia pthread_setcanceltype poate seta starea threadului la valoarea

PTHREAD_CANCEL_DEFERRED sau PTHREAD_CANCEL_ASYNCHRONOUS. Pentru

prima valoare, întreruperea se poate produce doar în puncte de abandon, iar pentru cea de a doua

valoare, întreruperea se produce imediat.

In rutina threadului, punctele de abandon se pot specifica explicit cu

pthread_testcancel().

Funcţiile de aşteptare precum pthread_join, pthread_cond_wait sau

pthread_cond_timedwait determină, de asemenea, puncte implicite de abandon.

Variabilele mutex şi funcţiile ataşate acestora nu generează puncte de abandon.

Marcarea pentru ştergere a unui thread se face prin apelul:

int pthread_detach(pthread_t tid);

Această funcţie marchează pentru ştergere structurile interne ale threadului. In acelaşi timp,

apelul informează nucleul că după terminare threadul tid va fi radiat, iar memoria ocupată de el

eliberată. Aceste acţiuni vor fi efectuate abia după ce threadul tid îşi termină în mod normal

activitatea.

Cea mai simplă metodă de sincronizare a thread-urilor este aşteptarea terminării unui thread. Ea

se realizează prin apeluri de tip join:

Posix API: int pthread_join(pthread_t tid, void **status);

Solaris API: int thr_join(thread_t tid, thread_t *realid, void **status);

Funcţia pthread_join suspendă execuţia threadului apelant până când threadul cu

descriptorul tid îşi încheie execuţia.

In cazul apelului thr_join, dacă tid este diferit de NULL, se aşteaptă terminarea threadului

tid. In caz contrar, se aşteaptă terminarea oricărui thread, descriptorul threadului terminat fiind

întors în parametrul realid.

Page 21: Programare Multi Threading

Programare multithreading - 21 –

Dublul pointer status primeşte ca valoare pointerul status transmis ca argument al apelului

pthread_exit, din interiorul threadului. In acest fel, threadul terminat poate transmite

apelatorului join o serie de date.

Thread-urile pentru care se apelează funcţiile join, nu pot fi create în starea detached.

Resursele unui thread joinable, care şi-a încheiat execuţia, nu se dealocă decât când pentru acest

thread este apelată funcţia join. Pentru un thread, se poate apela join o singură dată.

2.5.2.3 Un prim exemplu

Fără a intra deocamdată în prea multe amănunte, pregătim prezentarea programului

ptAtribLung.c, (programul 4.1), parametrizat de două constante (deci în patru variante

posibile), şi în care introducem utilizarea thread-urilor sub Unix.

//#define ATRIBLUNG 1

//#define MUTEX 1

#include <stdio.h>

#include <pthread.h>

typedef struct { char *s; int nr; int pas; int sec;}argument;

int p = 0;

pthread_mutex_t mutp = PTHREAD_MUTEX_INITIALIZER;

void f (argument * a) {

int i, x;

for (i = 0; i < a->nr; i++) {

#ifdef MUTEX

pthread_mutex_lock (&mutp);

#endif

x = p;

if (a->sec > 0)

sleep (random () % a->sec);

printf ("\n%s i=%d pas=%d", a->s, i, a->pas);

x += a->pas;

#ifdef ATRIBLUNG

p = x;

#else

p += a->pas;

#endif

#ifdef MUTEX

pthread_mutex_unlock (&mutp);

#endif

}

}

main () {

argument x = { "x:", 20, -1, 2 },

argument y = { "y:", 10, 2, 3 };

pthread_t th1, th2;

pthread_create ((pthread_t *) & th1, NULL, (void *) f,

(void *) &x);

pthread_create ((pthread_t *) & th2, NULL, (void *) f,

(void *) &y);

pthread_join (th1, NULL);

pthread_join (th2, NULL);

printf ("\np: %d\n", p);

}

Programul 2.1 Sursa primPThread.c

Page 22: Programare Multi Threading

Programare multithreading - 22 –

Este vorba de crearea şi lansarea în execuţie (folosind funcţia pthread_create) a două

thread-uri, ambele având aceeaşi acţiune, descrisă de funcţia f, doar cu doi parametri diferiţi.

După ce se aşteaptă terminarea activităţii lor (folosind funcţia pthread_join), se tipăreşte

valoarea variabilei globale p. Rezultatele pentru cele patru variante sunt prezentate în tabelul

următor. Pentru compilare, trebuie să fie specificată pe lângă sursă şi biblioteca

libpthread.so.0. (sau, pe scurt, se specifică numele pthread, la opţiunea -l).

Tabelul din figura 4.6 ilustrează funcţionarea programului în cele patru variante. In această

secţiune ne vor interesa doar primele două coloane.

In continuare, o să explicăm, pe rând, elementele introduse de către acest program, făcând astfel

primii paşi în lucrul cu thread-uri. Pentru o mai bună înţelegere, este bine ca din program să se

“reţină” din sursă numai liniile ce rămân în urma parametrizării.

ATRIBLUNG MUTEX ATRIBLUNG

MUTEX x: i=0 pas=-1

y: i=0 pas=2

x: i=1 pas=-1

y: i=1 pas=2

x: i=2 pas=-1

y: i=2 pas=2

x: i=3 pas=-1

y: i=3 pas=2

x: i=4 pas=-1

y: i=4 pas=2

x: i=5 pas=-1

x: i=6 pas=-1

y: i=5 pas=2

x: i=7 pas=-1

x: i=8 pas=-1

x: i=9 pas=-1

x: i=10 pas=-1

x: i=11 pas=-1

x: i=12 pas=-1

x: i=13 pas=-1

x: i=14 pas=-1

y: i=6 pas=2

y: i=7 pas=2

x: i=15 pas=-1

x: i=16 pas=-1

x: i=17 pas=-1

x: i=18 pas=-1

y: i=8 pas=2

x: i=19 pas=-1

y: i=9 pas=2

p: 0

x: i=0 pas=-1

y: i=0 pas=2

x: i=1 pas=-1

y: i=1 pas=2

x: i=2 pas=-1

x: i=3 pas=-1

x: i=4 pas=-1

y: i=2 pas=2

x: i=5 pas=-1

x: i=6 pas=-1

y: i=3 pas=2

x: i=7 pas=-1

x: i=8 pas=-1

y: i=4 pas=2

x: i=9 pas=-1

x: i=10 pas=-1

x: i=11 pas=-1

x: i=12 pas=-1

y: i=5 pas=2

x: i=13 pas=-1

x: i=14 pas=-1

x: i=15 pas=-1

y: i=6 pas=2

x: i=16 pas=-1

x: i=17 pas=-1

x: i=18 pas=-1

x: i=19 pas=-1

y: i=7 pas=2

y: i=8 pas=2

y: i=9 pas=2

p: 20

x: i=0 pas=-1

y: i=0 pas=2

x: i=1 pas=-1

y: i=1 pas=2

x: i=2 pas=-1

y: i=2 pas=2

x: i=3 pas=-1

y: i=3 pas=2

x: i=4 pas=-1

y: i=4 pas=2

x: i=5 pas=-1

y: i=5 pas=2

x: i=6 pas=-1

y: i=6 pas=2

x: i=7 pas=-1

y: i=7 pas=2

x: i=8 pas=-1

y: i=8 pas=2

x: i=9 pas=-1

y: i=9 pas=2

x: i=10 pas=-1

x: i=11 pas=-1

x: i=12 pas=-1

x: i=13 pas=-1

x: i=14 pas=-1

x: i=15 pas=-1

x: i=16 pas=-1

x: i=17 pas=-1

x: i=18 pas=-1

x: i=19 pas=-1

p: 0

x: i=0 pas=-1

y: i=0 pas=2

x: i=1 pas=-1

y: i=1 pas=2

x: i=2 pas=-1

y: i=2 pas=2

x: i=3 pas=-1

y: i=3 pas=2

x: i=4 pas=-1

y: i=4 pas=2

x: i=5 pas=-1

y: i=5 pas=2

x: i=6 pas=-1

y: i=6 pas=2

x: i=7 pas=-1

y: i=7 pas=2

x: i=8 pas=-1

y: i=8 pas=2

x: i=9 pas=-1

y: i=9 pas=2

x: i=10 pas=-1

x: i=11 pas=-1

x: i=12 pas=-1

x: i=13 pas=-1

x: i=14 pas=-1

x: i=15 pas=-1

x: i=16 pas=-1

x: i=17 pas=-1

x: i=18 pas=-1

x: i=19 pas=-1

p: 0

Figura 2.8 Comportări ale programului 4.1

Vom începe cu cazul în care nici una dintre constante nu este definită. In spiritul celor de mai

sus, variabila mutp nu este practic utilizată, vom reveni asupra ei în secţiunile următoare. De

asemenea, se vede că variabila p este modificată direct cu parametrul de intrare, fără a mai folosi

ca şi intermediar variabila x.

Din conţinutul programului se observă că funcţia f primeşte ca argument o variabilă incluzând în

structura ei: numele variabilei, numărul de iteraţii al funcţiei f şi pasul de incrementare al

variabilei p. Se vede că cele două thread-uri primesc şi cedează relativ aleator controlul.

Variabila x indică 20 iteraţii cu pasul –1, iar y 10 iteraţii cu pasul 2.

Page 23: Programare Multi Threading

Programare multithreading - 23 –

In absenţa constantei ATRIBLUNG, incrementarea lui p se face printr-o singură instrucţiune,

p+=a->pas. Este extrem de puţin probabil ca cele două thread-uri să-şi treacă controlul de la

unul la altul exact în timpul execuţiei acestei instrucţiuni. In consecinţă, variabila p rămâne în

final cu valoarea 0.

Prezenţa constantei ATRIBLUNG are menirea să “încurce” lucrurile. Se vede că incrementarea

variabilei p se face prin intermediul variabilei locale x. Astfel, după ce reţine în x valoarea lui p,

threadul stă în aşteptare un număr aleator de secunde şi abia după aceea creşte x cu valoarea a-

>pas şi apoi atribuie lui p noua valoare. In mod natural, în timpul aşteptării celălalt thread

devine activ şi îşi citeşte şi el aceeaşi valoare pentru p şi prelucrarea continuă la fel. Bineînţeles,

aceste incrementări întreţesute provoacă o mărire globală incorectă a lui p, motiv pentru care p

final rămâne cu valoarea 20. (De fapt, acest scenariu concret de aşteptări are drept consecinţă

faptul că efectul global coincide cu efectul celui de-al doilea thread.)

2.5.3 Instrumente standard de sincronizare

Instrumentele (obiectele) de sincronizare specifice thread-urilor sunt, aşa cum am arătat în 2.5:

variabilele mutex, variabilele condiţionale, semafoarele şi blocările cititor/scriitor (reader/writer).

Fiecare variabilă de sincronizare are asociată o coadă de thread-uri care aşteaptă - sunt blocate-

la variabila respectivă.

Cu ajutorul unor primitive ce verifică dacă variabilele de sincronizare sunt disponibile, fiecare

thread blocat va fi “trezit” la un moment dat, va fi şters din coada de aşteptare şi controlul lor va

fi cedat componentei de planificare. Trebuie remacat faptul că thread-urile sunt repornite într-o

ordine arbitrară, neexistând nici o relaţie între ordinea în care au fost blocate şi elimnarea lor din

coada de aşteptare.

O observaţie importantă! In situaţia în care un obiect de sincronizare este blocat de un thread,

iar acest thread îşi încheie fără a debloca obiectul, acesta - obiectul de sincronizare - va rămâne

blocat! Este deci posibil ca thread-urile blocate la obiectul respectiv vor intra în impas.

In continuare descriem primitivele de lucru cu aceste obiecte (variabile, entităţi) de sincronizare

pe platforme Unix, atât sub Posix, cât şi sub Solaris.

2.5.3.1 Operaţii cu variabile mutex

Iniţializarea unei variabile mutex se poate face static sau dinamic, astfel:

Posix, iniţializare statică: pthread_mutex_t numeVariabilaMutex = PTHREAD_MUTEX_INITIALIZER;

Posix, iniţializare dinamică: int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t

*mutexattr);

Solaris, iniţializare statică: mutex_t numeVariabilaMutex = 0; Solaris, iniţializare dinamică: int mutex_init(mutex_t *mutex, int type, void *arg);

Deci, iniţializarea statică presupune atribuirea unei valori standard variabilei mutex. Iniţializarea

dinamică se face apelând o funcţie de tip init, având ca prim argument un pointer la variabila

mutex.

Page 24: Programare Multi Threading

Programare multithreading - 24 –

Apelul pthread_mutex_init iniţializează variabila mutex cu atribute specificate prin

parametrul mutexattr. Semnificaţia acestei variabile o vom prezenta într-o secţiune

ulterioară. Pe moment acest argument are valoarea NULL, ceea ce semnifică fixarea de atribute

implicite.

Parametrul type din apelul mutex_init indică domeniul de vizibilitate al variabilei mutex.

Dacă are valoarea USYNC_PROCESS, atunci ea poate fi accesată din mai multe procese. Dacă

are valoarea USYNC_THREAD, atunci variabila este accesibilă doar din procesul curent.

Argumentul arg este rezervat pentru dezvoltări ulterioare, deci singura valoare permisă este

NULL.

Distrugerea unei variabile mutex înseamnă eliminarea acesteia şi eliberarea resurselor ocupate

de ea. In prealabil, variabila mutex trebuie să fie deblocată. Apelurile de distrugere sunt:

Posix: int pthread_mutex_destroy(pthread_mutex_t *mutex);

Solaris: int mutex_destroy(mutex_t *mutex);

Blocarea unei variabile mutex:

Posix: int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock (pthread_mutex_t *mutex);

Solaris: int mutex_lock(mutex_t *mutex);

int mutex_trylock(mutex_t *mutex);

In apelurile lock, dacă variabila mutex nu este blocată de alt thread, atunci ea va deveni

proprietatea threadului apelant şi funcţia returnează imediat. Dacă este deja blocată de un alt

thread, atunci funcţia intră în aşteptare până când variabila mutex va fi eliberată.

In apelurile trylock, funcţiile returnează imediat, indiferent dacă variabila mutex este sau nu

blocată de alt thread. Dacă variabila este liberă, atunci ea va deveni proprietatea threadului. Dacă

este blocată de un alt thread (sau de threadul curent), funcţia returnează imediat cu codul de

eroare EBUSY.

Deblocarea unei variabile mutex se realizează prin:

Posix: int pthread_mutex_unlock(pthread_mutex_t *mutex);

Solaris: int mutex_unlock(mutex_t *mutex);

Se presupune că variabila mutex a fost blocată de către threadul care apelează funcţia de

deblocare.

Este momentul să analizăm rezultatele din coloanele 3 şi 4 ale tabelului 4.6. In ambele variante

accesul la variabila p este exclusiv, fiind protejat de variabila mutp. Se observă că p final are

valoarea corectă. De asemenea, indiferent de faptul că ATRIBLUNG este sau nu definită (ceea ce

diferenţiază cele două cazuri), se observă o mare regularitate în succesiunea la control a thread-

urilor.

Intrebarea naturală care se pune este “ce se întamplă dacă mutex-ul este deja blocat de threadul

curent?”. Răspunsul diferă de la platformă la platformă. Programul 4.2 prezintă o situaţie bizară,

cititorul poate să-l testeze, dar să nu-l utilizeze în aplicaţii!:)

Page 25: Programare Multi Threading

Programare multithreading - 25 –

#include <synch.h>

#include <thread.h>

mutex_t mut;

thread_t t;

void* f(void* a) {

mutex_lock(&mut);

printf("lin 1\n");

mutex_lock(&mut); // 1

printf("lin 2\n");

mutex_unlock(&mut);

mutex_lock(&mut);

}

main() {

mutex_init(&mut,USYNC_THREAD,NULL);

thr_create(NULL,0,f,NULL,THR_NEW_lwp,&t);

thr_join(t,NULL,NULL);

}

Programul 2.2 Un (contra)exemplu: sursa dublaBlocareMutex.c

Punctăm ca observaţie faptul că, pe Solaris, execuţia acestui program produce impas în punctul

// 1.

2.5.3.2 Operaţii cu variabile condiţionale

Orice variabilă condiţională aşteaptă un anumit eveniment. Ea are asociată o variabilă mutex şi

un predicat. Predicatul conţine condiţia care trebuie să fie îndeplinită pentru a apărea

evenimentul, iar variabila mutex asociată are rolul de a proteja acest predicat. Scenariul de

aşteptare a evenimentului pentru care există variabila condiţională este:

Blochează variabila mutex asociată

Câttimp (predicatul este fals)

Aşteaptă la variabila condiţională

Execută eventuale acţiuni

Deblochează variabila mutex

Este de remarcat faptul că pe durata aşteptării la variabila condiţională, sistemul eliberează

variabila mutex asociată. In momentul în care se semnalizează îndeplinirea condiţiei, înainte de

ieşirea threadului din aşteptare, i se asociază din nou variabila mutex. Prin aceasta se permit în

fapt două lucruri: (1) să se aştepte la condiţie, (2) să se actualizeze predicatul şi să se semnalizeze

apariţia evenimentului.

Scenariul de semnalare - notificare - a apariţiei evenimentului este:

Blochează variabila mutex asociată

Fixează predicatul la true

Semnalizează apariţia evenimentului

la variabila condiţională

pentru a trezi thread-urile ce aşteaptă

Deblochează variabila mutex.

Iniţializarea unei variabile conditionale se poate face static sau dinamic, astfel:

Posix, iniţializare statică: pthread_cond_t numeVariabilaCond = PTHREAD_COND_INITIALIZER; Posix, iniţializare dinamică: int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *condattr);

Solaris, iniţializare statică:

cond_t numeVariabilaCond = DEFAULTCV ;

Page 26: Programare Multi Threading

Programare multithreading - 26 –

Solaris, iniţializare dinamică: int cond_init(cond_t *cond, int type, void *arg);

Deci, iniţializarea statică presupune atribuirea unei valori standard variabilei condiţionale.

Iniţializarea dinamică se face apelând o funcţie de tip init, având ca prim argument un pointer

la variabila condiţională.

Apelul pthread_cond_init iniţializează variabila condiţională cu atribute specificate prin

parametrul condattr. Semnificaţia acestei variabile o vom prezenta într-o secţiune ulterioară.

Pe moment acest argument are valoarea NULL, ceea ce semnifică fixarea de atribute implicite.

Parametrul type din apelul cond_init indică domeniul de vizibilitate al variabilei

condiţionale. Dacă are valoarea USYNC_PROCESS, atunci ea poate fi accesată din mai multe

procese. Dacă are valoarea USYNC_THREAD, atunci variabila este accesibilă doar din procesul

curent. Argumentul arg este rezervat pentru dezvoltări ulterioare, deci singura valoare permisă

este NULL.

Distrugerea unei variabile condiţionale înseamnă eliminarea acesteia şi eliberarea resurselor

ocupate de ea. In prealabil, variabila mutex trebuie să fie deblocată. Apelurile de distrugere sunt:

Posix: int pthread_cond_destroy(pthread_cond_t *cond);

Solaris: int cond_destroy(cond_t *cond);

Operaţia de aşteptare

Posix: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,

struct timespec*timeout);

Solaris: int cond_timedwait(cond_t *cond, mutex_t *mutex, timestruc_t *timeout);

Inainte de apelul funcţiilor de aşteptare (funcţii de tip wait), se cere blocarea variabilei mutex,

asociată variabilei condiţionale cond. După apelul unei funcţii de tip wait, se eliberează

variabila mutex şi se suspendă execuţia threadului până când condiţia aşteptată este îndeplinită,

moment în care variabila mutex este blocată din nou. Parametrul timeout furnizează un

interval maxim de aşteptare. Dacă condiţia nu apare (evenimentul nu se produce) în intervalul

specificat, aceste funcţii returnează un cod de eroare.

Operaţia de notificare

Posix: int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

Solaris: int cond_signal(cond_t *cond);

int cond_broadcast(cond_t *cond);

Funcţiile de tip signal anunţă îndeplinirea condiţiei după care se aşteaptă la cond. Dacă nici

un thread nu se afla în aşteptare, atunci nu se întâmplă nimic. Dacă sunt mai multe thread-uri

interesate, numai unul singur dintre acestea îşi va relua execuţia. Alegerea threadului care va fi

“trezit” depinde de implementare, de prioritaţi şi de politica de planificare. In cazul Solaris,

thread-urile legate sunt prioritare celor multiplexate pe lwp-uri.

Funcţiile de tip broadcast repornesc toate thread-urile care aşteaptă la cond.

Page 27: Programare Multi Threading

Programare multithreading - 27 –

In continuare vom prezenta un exemplu simplu, programul 4.3 ptVarCond.c. El descrie trei

thread-uri. Două dintre ele, ambele descrise de funcţia inccontor, incrementează de câte 7 ori

varibila contor. Al treilea thread, descris de funcţia watchcontor, aşteaptă evenimentul ca

variabila contor să ajungă la valoarea 12 şi semnalizează acest lucru.

#include <pthread.h>

int contor = 0;

pthread_mutex_t mutcontor = PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t condcontor = PTHREAD_COND_INITIALIZER;

int thid[3] = { 0, 1, 2 };

void incContor (int *id) {

int i; printf ("\nSTART incContor %d\n", *id);

for (i = 0; i < 7; i++) {

sleep(random() % 3);

pthread_mutex_lock (&mutcontor);

contor++;

printf("\n incContor: thread %d contor vechi %d contor nou %d",

*id, contor - 1, contor);

if (contor == 12)

pthread_cond_signal (&condcontor);

pthread_mutex_unlock (&mutcontor);

}

printf ("\nSTOP incContor %d\n", *id);

}

void verifContor (int *id) {

printf ("\nSTART verifContor \n");

pthread_mutex_lock (&mutcontor);

while (contor <= 12) {

pthread_cond_wait (&condcontor, &mutcontor);

printf ("\n verifContor: thread %d contor %d", *id, contor);

break;

}

pthread_mutex_unlock (&mutcontor);

printf ("\nSTOP verifContor \n");

}

main () {

pthread_t th[3];

int i;

//creaaza cele 3 thread-uri

pthread_create ((pthread_t *) & th[0], NULL,

(void *) verifContor, &thid[0]);

pthread_create ((pthread_t *) & th[1], NULL,

(void *) incContor, &thid[1]);

pthread_create ((pthread_t *) & th[2], NULL,

(void *) incContor, &thid[2]);

//asteapta terminarea thread-urilor

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

pthread_join (th[i], NULL);

}

Programul 2.3 Sursa ptVarCond.c

Rezultatul execuţiei programului este următorul:

START verifContor

START incContor 1

Page 28: Programare Multi Threading

Programare multithreading - 28 –

START incContor 2

incContor: thread 2 contor vechi 0 contor nou 1

incContor: thread 2 contor vechi 1 contor nou 2

incContor: thread 1 contor vechi 2 contor nou 3

incContor: thread 2 contor vechi 3 contor nou 4

incContor: thread 1 contor vechi 4 contor nou 5

incContor: thread 2 contor vechi 5 contor nou 6

incContor: thread 2 contor vechi 6 contor nou 7

incContor: thread 2 contor vechi 7 contor nou 8

incContor: thread 1 contor vechi 8 contor nou 9

incContor: thread 2 contor vechi 9 contor nou 10

STOP incContor 2

incContor: thread 1 contor vechi 10 contor nou 11

incContor: thread 1 contor vechi 11 contor nou 12

verifContor: thread 0 contor 12

STOP verifContor

incContor: thread 1 contor vechi 12 contor nou 13

incContor: thread 1 contor vechi 13 contor nou 14

STOP incContor 1

2.5.3.3 Operaţii cu semafoare

Spre deosebire de utilizarea semafoarelor Unix la nivel de proces descrise în 3.4, semafoarele

thread sunt mai simplu de utilizat, însă mai potrivite în interiorul aceluiaşi proces.

Iniţializare

Posix: int sem_init(sem_t *sem, int type, int v0);

Solaris: int sema_init(sema_t *sem, int v0, int type, void *arg);

Se iniţializează semaforul sem cu valoarea iniţială v0. Parametrul type din apelul Posix este 0

dacă semaforul este o resursă locală procesului şi diferit de 0, dacă semaforul poate fi partajat de

mai multe procese. în cazul thread-urilor Linux, valoarea este întotdeauna 0.

Acelaşi type din apelul Solaris are valorile posibile: USYNC_THREAD pentru resursă locală

sau USYNC_PROCESS pentru partajarea între procese. Parametrul arg are obligatoriu valoarea

NULL.

Se observă că pe Solaris este posibilă şi iniţializarea statică a semaforului, prin atribuirea valorii

iniţiale - obligatoriu - 0. In acest caz, semaforul este de tipul USYNC_THREAD.

Distrugerea semaforului

Posix: int sem_destroy(sem_t * sem);

Solaris: int sema_destroy(sema_t *sem);

Folosind acest apel, sunt eliberate resursele ocupate de semaforul sem. Dacă semaforul nu are

asociate resurse sistem, funcţiile destroy nu fac altceva decât să verifice dacă există thread-uri

care aşteaptă la semafor. In acest caz funcţia returnează eroarea EBUSY. In caz de semafor

invalid, întoarce eroarea EINVAL.

Incrementarea valorii semaforului (echivalentul operaţiei V - vezi 2.5.1)

Page 29: Programare Multi Threading

Programare multithreading - 29 –

Posix: int sem_post(sem_t * sem);

Solaris: int sema_post(sema_t *sem);

Funcţiile sem_post, respectiv sema_post incrementează cu 1 valoarea semaforului sem.

Dintre thread-urile blocate, planificatorul scoate unul şi-l reporneşte. Alegerea threadului

restartat depinde de parametrii de planificare.

Decrementarea valorii semaforului (echivalentul operaţiei P - vezi 2.5.1)

Posix: int sem_wait(sem_t * sem); int sem_trywait(sem_t * sem);

Solaris: int sem_wait(sema_t *sem); int sem_trywait(sema_t *sem);

Funcţiile sem_wait/sema_wait suspendă execuţia threadului curent până când valoarea

semaforului sem devine mai mare decât 0, după care decrementează atomic valoarea respectivă.

Funcţiile sem_trywait/sema_trywait sunt variantele fără blocare ale funcţiilor

sem_wait/ sema_wait. Dacă semaforul nu are valoarea 0, valoarea acestuia este

decrementată, altfel, funcţiile se termină cu codul de eroare EAGAIN.

In varianta Posix, există apelul:

int sem_getvalue(sem_t * sem, int *sval);

care depune valoarea curentă a semaforului sem în locaţia indicată de pointerul sval.

2.5.3.4 Blocare de tip cititor / scriitor (reader / writer)

Aceste obiecte (implementate atât în cazul thread-urilor Posix, cât şi Solaris) sunt folosite pentru

a permite mai multor thread-uri să acceseze, la un moment dat, o resursă partajabilă: fie în citire

de către oricâte thread-uri, fie numai de un singur thread care să o modifice.

Iniţializare

Posix: int pthread_rwlock_init(pthread_rwlock_t *rwlock,

pthread_rwlockattr_t *rwlockattr);

Solaris: int rwlock_init(rwlock_t *rwlock, int type, void *arg);

Se iniţializează obiectul rwlock. Parametrul rwlockattr din apelul Posix este purtătorul de

atribute al obiectului rwlock. Parametrul type din apelul Solaris are valorile posibile:

USYNC_THREAD pentru resursă locală sau USYNC_PROCESS pentru partajarea între

procese. Parametrul arg are obligatoriu valoarea NULL.

Distrugerea obiectului reader/writer

Posix: int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

Solaris: int rwlock_destroy(rwlock_t *rwlock);

Sunt eliberate resursele ocupate de obiectul rwlock.

Page 30: Programare Multi Threading

Programare multithreading - 30 –

Operaţia de blocare pentru citire presupune incrementarea numărului de cititori, dacă nici un

scriitor nu a blocat sau nu aşteaptă la obiectul reader/writer. In caz contrar, funcţiile lock intră

în aşteptare până când obiectul devine disponibil, iar funcţiile trylock întorc imediat cu cod

de eroare. Apelurile pentru această operaţie sunt:

Posix: int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

Solaris: int rw_rdlock(rwlock_t *rwlock);

int rw_tryrdlock(rwlock_t *rwlock);

Operaţia de blocare pentru scriere are rolul de a obţine obiectul reader/writer dacă nici un

thread nu l-a blocat în citire sau scriere. In caz contrar, fie se aşteaptă eliberarea obiectului In

cazul funcţiilor wrlock, fie întoarce imediat cu cod de eroare în cazul funcţiilor wrtrylock.

Apelurile pentru această operaţie sunt:

Posix: int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

Solaris: int rw_wrlock(rwlock_t *rwlock);

int rw_trywrlock(rwlock_t *rwlock);

2.5.4 Exemple de sincronizări

Ca exemplu de folosire a acestor mecanisme, vom rezolva o problemă generală de sincronizare:

m thread-uri accesează n resurse (m>n) în mai multe moduri, care conduc în final la rezultate

echivalente.

Pentru a trata această situaţie, rezolvăm o problema concretă “nrTr intră într-o gară prin nrLin

linii, nrTr>nrLin”, folosind diverse tehnici de sincronizare: semafoare, variabile mutex, etc.

1. Implementare sub Unix, folosind semafoare Posix, programul 4.4.

#include <semaphore.h>

#include <pthread.h>

#include <stdlib.h>

#define nrLin 5

#define nrTr 13

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

sem_t sem;

int poz[nrTr];

pthread_t tid[nrTr];

//afisare trenuri care intra in gara

void afisare() {

int i;

pthread_mutex_lock(&mutex);

printf("Trenuri care intra in gara:");

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

if (poz[i]==1)

printf(" %d",i);

printf("\n");

pthread_mutex_unlock(&mutex);

}

//rutina unui thread

void* trece(char* sind){

int sl,ind;

ind=atoi((char*)sind);

Page 31: Programare Multi Threading

Programare multithreading - 31 –

sem_wait(&sem);

poz[ind]=1;

afisare();

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

poz[ind]=2;

sem_post(&sem);

free(sind);

}

//main

main(int argc, char* argv[]) {

char* sind;

int i;

sem_init(&sem,0,nrLin);

for (i=0;i<nrTr;i++) {

sind=(char*) malloc(5*sizeof(char));

sprintf(sind,"%d",i);

pthread_create(&tid[i],NULL,trece,sind);

}

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

pthread_join(tid[i],NULL);

}

Programul 2.4 Sursa trenuriSemPosix.c

2. Implementare sub Unix folosind variabile mutex şi variabile condiţionale, programul 4.5.

#include <stdlib.h>

#include <pthread.h>

#include <errno.h>

#define nrLin 5

#define nrTr 13

pthread_mutex_t semm[nrLin];

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutexc=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t condElm=PTHREAD_COND_INITIALIZER;

int poz[nrTr];

pthread_t tid[nrTr];

int semmutex_lock() {

int i;

while (1) {

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

if (pthread_mutex_trylock(&semm[i])!=EBUSY)

return i;

pthread_mutex_lock(&mutexc);

pthread_cond_wait(&condElm,&mutexc);

pthread_mutex_unlock(&mutexc);

}

}

int semmutex_unlock(int i) {

pthread_mutex_unlock(&semm[i]);

pthread_cond_signal(&condElm);

}

//afisare trenuri care intra in gara

void afisare() {

int i;

pthread_mutex_lock(&mutex);

printf("Trenuri care intra in gara:");

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

if (poz[i]==1)

printf(" %d",i);

Page 32: Programare Multi Threading

Programare multithreading - 32 –

printf("\n");

pthread_mutex_unlock(&mutex);

}

//rutina unui thread

void* trece(char* sind){

int sl,ind,indm;

ind=atoi((char*)sind);

indm=semmutex_lock();

poz[ind]=1;

afisare();

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

poz[ind]=2;

semmutex_unlock(indm);

free(sind);

}

//main

main(int argc, char* argv[]) {

char* sind;

int i;

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

//semm[i]=PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_init(&semm[i],NULL);

for (i=0;i<nrTr;i++) {

sind=(char*) malloc(5*sizeof(char));

sprintf(sind,"%d",i);

pthread_create(&(tid[i]),NULL,trece,sind);

}

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

pthread_join(tid[i],NULL);

}

Programul 2.5 Sursa trenuriMutexCond.c

Programele 4.4 şi 4.5 conduc la rezultate de execuţie similare.

Exemplu rezultat execuţie pentru nrLin=5 şi nrTr=13.

Trenuri care intra in gara: 0

Trenuri care intra in gara: 0 1

Trenuri care intra in gara: 0 1 2

Trenuri care intra in gara: 0 1 2 3

Trenuri care intra in gara: 0 1 2 3 4

Trenuri care intra in gara: 0 2 3 4 5

Trenuri care intra in gara: 5 8

Trenuri care intra in gara: 5 7 8

Trenuri care intra in gara: 5 6 7 8

Trenuri care intra in gara: 5 6 7 8 9

Trenuri care intra in gara: 6 7 8 9 10

Trenuri care intra in gara: 7 8 9 10 11

Trenuri care intra in gara: 7 10 11 12

Se observă că la un moment dat intră în gară maximum 5 trenuri (câte linii sunt), iar execuţia

programului se încheie după ce toate cele 13 trenuri au intrat în gară.

2.5.5 Obiecte purtătoare de atribute Posix

In secţiunea legată de instrumentele standard de sincronizare (4.3.3), am amânat prezentarea unui

anumit parametru şi am promis că vom reveni asupra lui. Este vorba de:

Page 33: Programare Multi Threading

Programare multithreading - 33 –

apelul pthread_create (4.3.2.1), argumentul pointer la tipul pthead_attr_t pe care

l-am numit attr;

apelul pthread_mutex_init (4.3.3.1), argumentul pointer la tipul

pthead_mutexattr_t pe care l-am numit mutexattr;

apelul pthread_cond_init (4.3.3.2), argumentul pointer la tipul

pthead_condattr_t pe care l-am numit condattr;

apelul pthread_rwlock_init (4.3.3.4), argumentul pointer la tipul

pthead_rwlockattr_t pe care l-am numit rwlockattr;

In exemplele de până acum am folosit pentru aceste argumente valoarea NULL, lăsând sistemul

să fixeze valori implicite.

O variabilă având unul dintre tipurile enumerate mai sus este numită, după caz, obiect purtător

de atribute: thread, mutex, cond, rwlock. Un astfel de obiect păstrează o serie de caracteristici,

fixate de către programator. Aceste caracteristici trebuie transmise în momentul creării sau

iniţializării threadului sau obiectului de sincronizare respectiv. In succesiune logică, crearea unui

astfel de obiect precede crearea threadului sau obiectului de sincronizare care va purta aceste

atribute.

Numai modelul Posix utilizează acest mecanism. Modelul Solaris include aceste atribute printre

parametrii de creare a threadului, respectiv de iniţializare a obiectului de sincronizare respectiv.

Obiectele purtătoare de atribute au avantajul că Imbunătăţesc gradul de portabilitate a codului.

Apelul de creare a unui thread / iniţializarea unui obiect de sincronizare rămâne acelaşi,

indiferent de modul cum este implementat obiectul purtător de atribute. Un alt avantaj este faptul

ca un obiect purtător de atribute se iniţializează simplu, o singură dată şi poate fi folosit la

crearea mai multor thread-uri. La terminarea thread-urilor, trebuie eliberată memoria alocată

pentru obiectele purtătoare de atribute.

Asupra unui obiect purtător de atribute, indiferent care dintre cele trei de mai sus, se pot efectua

operaţiile:

Iniţializare (operaţie init)

Distrugere (operaţie destroy)

Setarea valorilor unor atribute (operaţie set)

Obţinerea valorilor unor atribute (operaţie get)

2.5.5.1 Iniţializarea şi distrugerea unui obiect atribut

Mecanismul de iniţializare este foarte asemănător la aceste tipuri de obiecte. Mai întâi este

necesară, după caz, declararea unei variabile (vom folosi aceleaşi nume ca în prototipurile din

4.3.2):

pthread_attr_t attr;

pthread_mutexattr_t mutexattr;

pthread_condattr_t condattr;

pthread_rwlockattr_t rwlockattr;

Apoi se transmite adresa acestei variabile prin apelul sistem corespunzător:

int pthread_attr_init(pthread_attr_t &attr);

int pthread_mutexattr_init(pthread_mutexattr_t &mutexattr);

int pthread_condattr_init(pthread_condattr_t &condattr);

int pthread_rwlockattr_init(pthread_rwlockattr_t &rwlockattr);

Page 34: Programare Multi Threading

Programare multithreading - 34 –

Distrugerea se face, după caz, folosind unul dintre apelurile sistem:

int pthread_attr_destroy(pthread_attr_t &attr);

int pthread_mutexattr_destroy(pthread_mutexattr_t &mutexattr);

int pthread_condattr_destroy(pthread_condattr_t &condattr);

int pthread_rwlockattr_destroy(pthread_rwlockattr_t &rwlockattr);

2.5.5.2 Gestiunea obiectelor purtătoare de atribute thread

Unui thread i se pot fixa, printre altele, o serie de atribute privind politica de planificare, atribute

de moştenire, componenta care gestionează threadul, priorităţi şi caracteristici ale stivei proprii.

In apelurile sistem care urmează, vom nota cu attr referinţa la un obiect purtător de atribut, iar

prin p parametrul specific atributului.

Pentru fixarea politicii de planificare este folosit apelul sistem:

int pthread_attr_setpolicy(pthread_condattr_t *attr, int p);

int pthread_attr_getpolicy(pthread_condattr_t *attr, int *p);

Politica p, atribuită obiectului referit prin attr, este specificată (set) prin una din următoarele

constante:

SCHED_FIFO dacă se doreşte planificarea primul venit – primul servit

SCHED_RR dacă se doreşte planificarea "Round-Robin" (servire circulară a fiecăruia câte o

cuantă de timp).

SCHED_OTHERS dacă se doreşte o anumită politică specială (nu ne ocupăm de ea).

Pentru a se cunoaşte politica fixată (get) dintr-un obiect atribut attr, aceasta se depune în

întregul indicat de pointerul p.

Fixarea moştenirii politicii de planificare se face prin:

int pthread_attr_setinheritsched(pthread_attr_t *attr, int p);

int pthread_attr_getinheritsched(pthread_attr_t *attr, int *p);

La set, p poate avea valorile:

PTHREAD_INHERIT_SCHED politica de planificare este moştenită de la procesul creator.

PTHREAD_EXPLICIT_SCHED politica trebuie specificată explicit.

Valoarea tipului de moştenire fixat se obţine prin get în p.

Fixarea domeniului de vizibilitate a threadului: Componenta care gestionează thread-urile nou

create poate fi ori procesul curent, ori nucleul sistemului. Pentru a specifica unul dintre ele se

utilizează apelul sistem:

int pthread_attr_setscope(pthread_attr_t *attr, int p);

int pthread_attr_getscope(pthread_attr_t *attr, int *p);

La set, p poate avea una dintre valorile:

PTHREAD_SCOPE_PROCESS - thread user, local procesului.

PTHREAD_SCOPE_SYSTEM - thread nucleu.

Obţinerea valorii curente se face prin get, depunând valoarea în întregul punctat de p.

Fixarea statutului unui thread în momentul terminării acţiunii lui se face folosind:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int p);

Page 35: Programare Multi Threading

Programare multithreading - 35 –

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *p);

La set, parametrul p indică acest statut prin una dintre valorile:

PTHREAD_CREATE_DETACHED - threadul este distrus la terminare.

PTHREAD_CREATE_JOINABLE - threadul este păstrat după terminare.

Statutul unui thread se obţine în variabila indicată de p prin metota get.

Parametrii stivei unui thread pot fi manevraţi prin apelurile:

int pthread_attr_setstackaddr(pthread_attr_t *attr, void *p);

int pthread_attr_setstacksize(pthread_attr_t *attr, int p);

int pthread_attr_getstackaddr(pthread_attr_t *attr, void **p);

int pthread_attr_getstacksize(pthread_attr_t *attr, int *p);

Este vorba de fixarea adresei stivei şi a alungimii acesteia, respectiv de obţinerea acestor valori.

Fixarea unei priorităţi o vom prezenta într-o secţiune destinată special planificării thread-urilor.

2.5.5.3 Gestiunea obiectelor purtătoare de atribute mutex, cond, rdwr

Fixarea protocolului de acces la mutex:

int pthread_mutexattr_setprotocol(pthread_attr_t *mutexattr, int p);

int pthread_mutexattr_getprotocol(pthread_attr_t *mutexattr, int *p);

Pentru set, parametrul p poate lua valorile:

PTHREAD_PRIO_INHERIT, dacă prioritatea este moştenită de la threadul creator.

PTHREAD_PRIO_NONE, dacă nu se foloseşte nici un protocol de prioritate.

PTHREAD_PRIO_PROTECT, dacă se foloseşte un protocol explicit.

Obţinerea valorii setate se face prin metoda get, care depune valoarea în p.

Fixarea domeniului de utilizare:

int pthread_mutexattr_setpshared(pthread_attr_t *mutexattr, int p);

int pthread_mutexattr_getpshared(pthread_attr_t *mutexattr, int *p);

La set, parametrul p poate lua valorile:

PTHREAD_PROCESS_PRIVATE, dacă se foloseşte numai în procesul curent.

PTHREAD_PROCESS_SHARED, dacă se foloseşte şi în alte procese.

Valoarea de partajare se obţine prin get, care depune valoarea în p.

O variabilă condiţională se poate folosi nu numai în procesul curent, ci şi în alte procese. Această

modalitate de partajare este gestionată prin apelurile:

int pthread_condattr_setpshared(pthread_attr_t *condattr, int p);

int pthread_condattr_getpshared(pthread_attr_t *condattr, int *p);

La set, parametrul p poate lua valorile:

PTHREAD_PROCESS_PRIVATE, dacă variabila condiţională se va folosi numai în

procesul curent.

PTHREAD_PROCESS_SHARED, dacă ea se va folosi şi în alte procese.

Valoarea de partajare se obţine în p, prin metoda get.

Page 36: Programare Multi Threading

Programare multithreading - 36 –

O variabilă rwlock se poate folosi nu numai în procesul curent, ci şi în alte procese. Această

modalitate de partajare este gestionată prin apelurile:

int pthread_rwlockattr_setpshared(pthread_attr_t *condattr, int p);

int pthread_rwlockattr_getpshared(pthread_attr_t *condattr, int *p);

La set, parametrul p poate lua valorile:

PTHREAD_PROCESS_PRIVATE, dacă variabila condiţională se va folosi numai în

procesul curent.

PTHREAD_PROCESS_SHARED, dacă ea se va folosi şi în alte procese.

Valoarea de partajare se obţine în p, prin metoda get.

2.5.6 Planificarea thread-urilor sub Unix

Există, în principiu, trei politici de planificare a thread-urilor, desemnate prin trei constante

specifice:

SCHED_OTHER, sau echivalent SCHED_TS politică implicită time-sharring, non real-time.

SCHED_RR (round-robin) planificare circulară, preemptivă, politică real-time: sistemul de

operare întrerupe execuţia la cuante egale de timp şi dă controlul altui thread.

SCHED_FIFO (first-in-first-out) planificare cooperativă, threadul în execuţie decide cedarea

controlului spre următorul thread.

Pentru fixarea politicilor real-time este nevoie ca procesul să aibă privilegiile de superuser [17,

110].

Atât sub Posix, incluzând aici Linux cât şi pe Solaris există funcţii specifice pentru modificarea

priorităţii thread-urilor după crearea acestora.

2.5.6.1 Gestiunea priorităţilor sub Posix

Modelul Posix foloseşte în acest scop obiectele purtătoare de atribute ale threadului, despre care

am vorbit în 4.3.5. Este vorba de o funcţie de tip set care fixează prioritatea şi de o funcţie de

tip get care obţine valoarea acestei priorităţi:

int pthread_attr_setschedparam(pthread_attr_t *attr,

struct sched_param *p);

int pthread_attr_getschedparam(pthread_attr_t *attr,

struct sched_param *p);

Structura sched_param este: struct sched_param {

int sched_priority;

}

Câmpul sched_priority conţine valoarea curentă a priorităţii.

Pentru thread-uri sunt fixate 32 nivele de priorităţi. Valorile concrete ale numerelor de prioritate

nu sunt nişte numere prefixate, ci depind de implementare. Pentru a le putea manevra,

utilizatorul trebuie să obţină mai întâi valorile extreme, după care să stabilească el, în mod

proporţional, cele 32 de valori ale priorităţilor. Valorile extreme se obţin prin apelurile:

Page 37: Programare Multi Threading

Programare multithreading - 37 –

sched_get_priority_max(SCHED_FIFO);

sched_get_priority_min(SCHED_FIFO);

sched_get_priority_max(SCHED_RR);

sched_get_priority_min(SCHED_RR);

2.5.6.2 Gestiunea priorităţilor sub Solaris

Prioritatea implicită pentru thread-urile multiplexate (libere), este 63. Priorităţile thread-urilor

legate sunt stabilite de sistem.

Pentru modificarea/obţinerea priorităţii unui thread se pot folosi şi funcţiile:

int thr_setprio(thread_t tid, int prio);

int thr_getprio(thread_t tid, int *prio);

void thr_get_rr_interval(timestruc_t *rr_time);

Ultima funcţie se foloseşte doar pentru politica de planificare round-robin, care furnizează în

structura punctată de rr_time intervalul de timp în milisecunde şi nanosecunde de aşteptare

pentru fiecare thread înainte de a fi planificat.

2.5.6.3 Exemplu de planificare manuală a thread-urilor pe Solaris

In exemplul din programul 4.6 se vor creea trei thread-uri în starea suspendat. Se vor modifica

priorităţile thread-urilor, după care ele vor fi lansate în execuţie. In urma execuţiei, se va constata

că de fapt sistemul nu prea ţine cont, pentru planificarea thread-urilor, de priorităţile fixate

explicit de către programator.

#include <thread.h>

int lev;

void* f1(void * arg) {

printf("begin ............ %s\n",arg);

sleep(10);

printf("end ............ %s\n",arg);

}

main() {

thread_t t1,t2,t3;

// 0 lwp in pool; 1 lwp vazut cu trace

//cu THR_NEW_lwp ordinea este 2 ,3, 1 dupa sleep ,2,1,3

//cu THR_BOUND ordinea este 1,2 ,3 dupa sleep ,2,3,1

thr_create(NULL,0,f1,"1",THR_SUSPENDED,&t1);

thr_setprio(t1,100);

thr_create(NULL,0,f1,"2",THR_SUSPENDED,&t2);

thr_setprio(t2,1000);

thr_create(NULL,0,f1,"3",THR_SUSPENDED,&t3);

thr_setprio(t3,1);

thr_continue(t1); thr_continue(t3);thr_continue(t2);

thr_join(t1,NULL,NULL);

thr_join(t2,NULL,NULL);

thr_join(t3,NULL,NULL);

lev=thr_getconcurrency();

printf("%d\n",lev);

}

Programul 2.6 Sursa planificareSolaris.c

Page 38: Programare Multi Threading

Programare multithreading - 38 –

2.5.7 Facilităţi speciale ale lucrului cu thread-uri Unix

2.5.7.1 Execuţie cel mult o dată

Acest gen de execuţie este un mecanism de sincronizare specific thread-urilor Posix, care

permite unei funcţii de iniţializare să fie executată o dată şi numai odată, indiferent câte thread-

uri o apelează.

Utilizarea lui este simplă. Mai întâi, programatorul trebuie să-şi definească funcţia specifică de

iniţializare, aici Ii vom da numele f. Apoi, trebuie să-şi definească o variabilă statică, într-un loc

de unde să fie vizibilă de către toate rutinele care apelează funcţia de iniţializare. Această

variabilă defineşte un bloc de control prin care se controlează execuţia unică. Declararea unei

astfel de variabile, căreia i-am dat numele once, se face astfel:

static pthread_once_t once = PTHREAD_ONCE_INIT;

Funcţia (f) care descrie iniţializarea unică, trebuie să fie declarată “pthread_once”. Aceasta se

realizează prin apelul:

int pthread_once(pthread_once_t *once, void(*f)(void));

f este numele funcţiei de iniţializare, iar once este blocul care controlează execuţia unică. Cu

excepţia primului, celelalte thread-uri care invocă funcţia f Impreună cu blocul once vor fi

inefective.

Secţiunea următoare va conţine un exemplu de utilizare a acestei facilităţi.

2.5.7.2 Asociere date specifice thread-urilor cu chei

Implementarea Unix a thread-urilor oferă funcţii pentru crearea de date specifice thread-urilor.

Această facilitate se poate folosi atunci când programul multi-thread cere ca anumite date

globale, statice, să aibă valori diferite în thread-uri diferite. In acest scop, fiecare thread dispune

de o zonă de memorie privată numită pe scurt TSD, acronim de la Thread Specific Data. Această

zonă de memorie este indexată după nişte chei TSD care asociază acestor chei pointeri void*.

Cheile sunt comune tuturor thread-urilor, dar valorile cheilor diferă de la un thread la altul.

In momentul când un thread este creat, se asociază valoarea NULL, tuturor cheilor sale din zona

TSD. Prototipurile funcţiilor care gestionează cheile TSD sunt:

Posix:: int pthread_key_create(pthread_key_t *key, void (*destructor) (void*);

int pthread_setspecific(pthread_key_t key, void *value);

void* pthread_getspecific(pthread_key_t key);

int pthread_key_delete(pthread_key_t key);

Solaris: int thr_keycreate(thread_key_t *key, void (*destructor)(void *));

int thr_setspecific(thread_key_t key, void *value);

int thr_getspecific(thread_key_t key, void **value);

int thr_keydelete(thread_key_t key);

Page 39: Programare Multi Threading

Programare multithreading - 39 –

Alocarea unei chei, ca index în zona TSD, se face cu funcţiile pthread_key_create sub

Posix şi key_create sub Solaris. Parametrul key punctează spre cheia creată, destructor

este o funcţie care se va apela automat la terminarea thread-urilor care folosesc cheia. Inainte ca

destructorul să fie apelat, pentru cheie este asociată valoarea NULL.

Pentru dealocarea unei chei, se apelează funcţiile pthread_key_delete, respectiv

thr_key_delete. In cazul Posix, funcţia pthread_key_delete nu verifică dacă cheia

are asociate date cu valoarea diferită de NULL, nici nu apelează destructorul. Pe Solaris, dacă

cheia are asociată o valoare, atunci când se încearcă distrugerea ei, funcţia returnează o eroare cu

codul EBUSY.

Asocierea unei noi valori la o anumită cheie în threadul curent, se realizează cu funcţiile

pthread_setspecific, respectiv thr_setspecific, unde semnificaţia parametrilor

este următoarea: key este cheia (obţinută cu keycreate) indice în zona TSD, iar value este

un pointer la valoarea care se asociază valoarea cheii sau NULL.

Pentru obţinerea valorii specifice threadului apelant, valoare asociată unei chei precizate ca

parametru, se folosesc funcţiile pthread_getspecific, respectiv thr_getspecific. In

caz de succes, la Posix este returnat un pointer spre valoarea specifică threadului, iar la Solaris

este întors un pointer la adresa valorii. In caz de insucces, de exemplu dacă cheia nu are asociate

date sau este invalidă, valoarea returnată este NULL la Posix, respectiv o valoare negativă în

cazul Solaris, iar variabila errno este setată corespunzător.

In continuare prezentăm câteva exemple de asociere date - chei la thread-uri.

1. Se cere un program multi-thread, în care fiecare thread furnizează numere consecutive,

începând de la 0. Sursa lui este dată în programul 4.7.

#include <pthread.h>

pthread_t user_threadID1,user_threadID2,user_threadID3;

static pthread_mutex_t contor_mutex = PTHREAD_MUTEX_INITIALIZER;

static pthread_key_t contor_key;

int contor_init = 0;

//folosirea unor variabile statice in functia threadului

//pentru aceasta versiune a functiei contor

//se returneaza urmatorul numar per-proces si nu per-thread

int contor1 () {

static int i=0;

return i++;

}

void destr(void* v){

static ap=0;

ap++;

printf("...Destructor nr apel %d, din thread

%d...\n",ap,pthread_self());

}

void *v;

//noul contor folosind date specifice thread-urilor

//pentru un anumit thread, returneaza urmatorul numar întreg

//ind reprezinta valoarea de start

int contor(int ind) {

int *i;

i = (int *)pthread_getspecific(contor_key);

Page 40: Programare Multi Threading

Programare multithreading - 40 –

if (!i) {

i = (int*)malloc(sizeof(int));

v=i;

*i=0;

pthread_setspecific(contor_key, i);

return 0;

}

*i=*i+1;

pthread_setspecific(contor_key, i);

return *i;

}

//rutina asociata thread-urilor

void * handler1(char argv[]) {

printf("thread: %d, valoare specifica: %d\n",pthread_self(),

contor(atoi(argv)));

sleep(1);

printf("thread: %d, valoare specifica: %d\n",pthread_self(),

contor(atoi(argv)));

sleep(3);

printf("thread: %d, valoare specifica: %d\n",pthread_self(),

contor(atoi(argv)));

printf("thread: %d, valoare specifica: %d\n",pthread_self(),

contor(atoi(argv)));

}

main( int argc, char *argv[] ) {

//crearea cheii

if (contor_init == 0)

if (!pthread_key_create(&contor_key, destr))

contor_init = 1;

pthread_create(&user_threadID1, NULL, handler1, "0");

pthread_create(&user_threadID2, NULL, handler1, "0");

pthread_create(&user_threadID3, NULL, handler1, "0");

pthread_join(user_threadID1,NULL);

pthread_join(user_threadID2,NULL);

pthread_join(user_threadID3,NULL);

free(v);

}

Programul 2.7 Sursa AsociereDateChei.c

Rezultatul execuţiei este:

thread: 1026, valoare specifica: 0

thread: 2051, valoare specifica: 0

thread: 3076, valoare specifica: 0

thread: 2051, valoare specifica: 1

thread: 1026, valoare specifica: 1

thread: 3076, valoare specifica: 1

thread: 1026, valoare specifica: 2

thread: 1026, valoare specifica: 3

...Destructor nr apel 1, din thread 1026...

thread: 2051, valoare specifica: 2

thread: 2051, valoare specifica: 3

...Destructor nr apel 2, din thread 2051...

thread: 3076, valoare specifica: 2

thread: 3076, valoare specifica: 3

...Destructor nr apel 3, din thread 3076...

2. Alocarea unui buffer de 100 caractere, specific fiecărui thread din program. Buffer-ul va fi

automat dealocat la terminarea threadului. Sursa este dată în programul 4.8.

Page 41: Programare Multi Threading

Programare multithreading - 41 –

#include <pthread.h>

static pthread_key_t buf_key; //cheia pentru buffer

static pthread_once_t buf_key_once = PTHREAD_ONCE_INIT;

//eliberare bufer

static void bufer_destroy(void * buf){

free(buf);

}

//alocare cheie

static void bufer_key_aloc() {

pthread_key_create(&buf_key, bufer_destroy);

}

//alocare bufer

void bufer_aloc(void) {

pthread_once(&buf_key_once, bufer_key_aloc);

pthread_setspecific(buf_key, malloc(100));

}

//obtinere bufer

char * get_bufer(void) {

return (char *) pthread_getspecific(buf_key);

}

Programul 2.8 Sursa Asocierepthread_once.c

2.5.7.3 Operaţii specifice lwp sub Solaris

In secţiunile precedente am precizat faptul că pe Solaris, thread-urile create în spaţiu utilizator

sunt multiplexate pe mai multe thread-uri nucleu, numite light-weight processes (lwp). Tot

atunci am punctat mai multe caracteristici ale acestor lwp-uri. In această secţiune vom descrie

modul de folosire a lwp-urilor direct în program. Pentru a putea folosi funcţiile specifice lwp în

program, trebuie incluse fişierele header <ucontext.h> şi <sys/lwp.h>.

Crearea lwp-urilor

Inainte de crearea propriu-zisă a unui lwp, se construieşte contextul acestuia cu funcţia:

void _lwp_makecontext(ucontext_t *ucp, void (*start_routine)

(void *arg),void *arg, void *private,

caddr_t stackbase, size_t stacksize);

unde ucp este un pointer la contextul care se va crea, start_routine este funcţia care se va

apela la crearea lwp-ului, iar arg argumentul acesteia. Parametrul private, în majoritatea

implementărilor, reţine adresa descriptorului de lwp. Stiva lwp-ului este punctată de

stackbase, are dimensiunea stacksize şi trebuie alocată dinamic.

Pentru crearea unui lwp, se foloseşte funcţia:

int _lwp_create(ucontext_t *context, long flags, lwpid_t *new_lwp);

Semnificaţia parametrilor: context este un pointer la contextul noului lwp creat cu

_lwp_makecontext, flags reprezintă flaguri de creare şi new_lwp este un pointer la

identificatorul noului lwp returnat de _lwp_create.

Flagurile de creare pot primi valorile:

lwp_DETACHED caz în care resursele lwp-urilor vor fi dealocate imediat ce lwp-urile îşi

încheie execuţia;

Page 42: Programare Multi Threading

Programare multithreading - 42 –

lwp_SUSPENDED caz în care lwp-ul va fi creat în starea suspendat. Pentru continuarea

execuţiei, este nevoie de un apel _lwp_continue, prezentat mai jos.

int _lwp_continue(lwpid_t target_lwp);

Apelul complementar pentru suspendarea unui lwp este:

int _lwp_suspend(lwpid_t target_lwp);

Aşteptarea terminării este similară apelurilor join şi se face cu apelul:

int _lwp_wait(lwpid_t tid, lwpid_t *realid);

Dacă tid este diferit de NULL, atunci se aşteaptă terminarea lwp-ului tid. In caz contrar, se

aşteaptă terminarea oricărui lwp, descriptorul lui fiind întors în parametrul realid.

Spre deosebire de thread-urile user-level, funcţia lwp-urilor nu returnează nici o valoare, lipseşte

parametrul status (prezent la join).

Terminarea unui lwp se poate specifica prin:

void _lwp_exit(void);

Funcţia _lwp_exit nu returnează nici un cod de terminare a lwp-ului. Dacă apelantul funcţiei

_lwp_exit este ultimul lwp care se execută, procesul curent se termină.

In continuare, vom concretiza discuţia despre lwp-uri printr-un exemplu simplu de creare a unui

lwp, ilustrat în programul 4.9.

#include <sys/lwp.h>

#include <ucontext.h>

#include <sys/types.h>

#include <stdio.h>

#include <signal.h>

//functia de control a lwp-ului

void func(void *arg) {

printf("Sunt un lwp.\n");

return ;

}

main() {

int i;

ucontext_t *contextp;

lwpid_t lwp_nou,n;

char privat[]="da",arg[]="arg";

int error;

caddr_t stack_base;

int stack_size=2000;

//se aloca spatiu pentru context

contextp = (ucontext_t *)malloc(sizeof(ucontext_t));

//se aloca spatiu pentru stiva lwp-ului

stack_base = (caddr_t)malloc(stack_size);

//in contextul lwp-ului se seteaza masca de semnale a

//procesului curent...

sigprocmask(SIG_SETMASK, NULL, &contextp->uc_sigmask);

//se creeaza contextul

_lwp_makecontext(contextp, func, NULL, privat, stack_base,stack_size);

//se creeza lwp-ul (se va lansa in executie functia func)

Page 43: Programare Multi Threading

Programare multithreading - 43 –

error = _lwp_create(contextp, NULL, &lwp_nou);

printf("Cod de retur %d.\n",error);

//se asteapta terminarea lwp-ului

_lwp_wait(lwp_nou,NULL);

}

Programul 2.9 Sursa exempluLwp.c

Rezultatul execuţiei este:

Cod de retur 0.

Sunt un lwp.

Ca observaţie, punctăm faptul că o dimensionare necorespunzătoare a stivei (valorea prea mică

pentru această dimensiune) conduce la o eroare de tipul “segmentation fault”.

In plus faţă de apelurile de terminare (aşteptarea terminării) a execuţiei unui thread, în cazul

thread-urilor Solaris, se permite suspendarea, respectiv continuarea execuţiei unui thread.

Solaris API: int thr_suspend(thread_t target_thread);

int thr_continue(thread_t target_thread);

Apelurile care permit aceste operaţii sunt thr_suspend(), respectiv thr_continue(),

cu argument identificatorul threadului, asupra căruia se va efectua operaţia. In plus, cu ajutorul

apelului thr_yield(), threadul curent cedează resursa procesor, în favoarea altui thread, care

este în starea runnable.

2.5.8 Exemple clasice de lucru cu thread-uri

2.5.8.1 Adunarea în paralel a n numere

Este vorba de evaluarea în paralel a unei expresii aritmetice, problemă despre care am vorbit şi în

4.2.1. Aici vom prezenta, prin programul 4.10, implementarea ei cu thread-uri Posix. La

execuţie, se precizează un argument n, iar programul calculează, în paralel, suma 1+1+...+1=n.

#include <pthread.h>

pthread_t *tid;

typedef

struct {

int i,st,dr,astept,sa,da;

} Thr_info;

static int* a;

//rutina unui thread

void* aduna(void* inf) {

int i, sa, da, st = 0, dr = 0, status, astept;

i=((Thr_info*)inf)->i;

Page 44: Programare Multi Threading

Programare multithreading - 44 –

astept=((Thr_info*)inf)->astept;

if (astept) {

st=((Thr_info*)inf)->st;

dr=((Thr_info*)inf)->dr;

while (tid[st]==-1);

while (tid[dr]==-1);

pthread_join(tid[st],NULL);

pthread_join(tid[dr],NULL);

}

sa=((Thr_info*)inf)->sa;

da=((Thr_info*)inf)->da;

a[sa]=a[sa]+a[da];

printf("Thread %d cu ascendenti %d

%d.\n",pthread_self(),tid[st],tid[dr]);

free(inf);

}

//functia paralel apelata din main, in care se creeaza thread-urile si se

//atribuie acestora "sarcini" (mai precis, ce numere sa adune)

paralel(char arg[]) {

int n, l, m, k, i, j, dk, dlmk;

Thr_info *inf;

n=atoi(arg);

for (l = 0, m = 1; n > m; l++, m *= 2);

a=(int*) malloc(m*sizeof(int));

tid=(pthread_t*) malloc(m*sizeof(pthread_t));

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

tid[i]=-1;

for (i = 0; i < n; i++) a[i] = 1;

for (i = n; i < m; i++) a[i] = 0;

for (k = 0, i = 1, n = 1, dk = 1, dlmk = m; k < l;

k++, dk *= 2, dlmk /= 2, n *= 2)

for (j = 0; j < n; j++, i++) {

inf=(Thr_info*)malloc(sizeof(Thr_info));

inf->i=i; inf->st=2*i; inf->dr=2*i+1;

inf->astept=(k<l-1)?1:0;

inf->sa=dlmk*(i-dk);

inf->da=dlmk/2*(2*i+1-dk*2);

pthread_create(&tid[i], NULL, aduna, inf);

}

pthread_join(tid[1],NULL);

printf("...Pentru n=%d, totalul=%d...\n",n,a[0]);

free(a);

free(tid);

}

//functia principala "main"

main(int argc, char* argv[]) {

paralel(argv[1]);

}

Programul 2.10 Sursa SumaN.c

Exemplu de rezultat al execuţiei: Thread 8201 cu ascendenti -1 -1.

Thread 9226 cu ascendenti -1 -1.

Thread 4101 cu ascendenti 8201 9226.

Thread 10251 cu ascendenti -1 -1.

Page 45: Programare Multi Threading

Programare multithreading - 45 –

Thread 11269 cu ascendenti -1 -1.

Thread 5126 cu ascendenti 10251 11269.

Thread 2051 cu ascendenti 4101 5126.

Thread 12297 cu ascendenti -1 -1.

Thread 13315 cu ascendenti -1 -1.

Thread 14341 cu ascendenti -1 -1.

Thread 6151 cu ascendenti 12297 13315.

Thread 15366 cu ascendenti -1 -1.

Thread 7176 cu ascendenti 14341 15366.

Thread 3076 cu ascendenti 6151 7176.

Thread 1026 cu ascendenti 2051 3076.

...Pentru n=10, totalul=10...

Evident că de la o execuţie la alta, ceea ce diferă este ordinea în care se execută thread-urile

independente, dar suma obţinută în final va fi aceeaşi (n).

2.5.8.2 Problema producătorilor şi a consumatorilor

Prezentăm, în programul 4.11, soluţia problemei producătorilor şi consumatorilor, problemă

enunţată în capitolul 1.4. Această soluţie este implementată folosind thread-uri Posix.

#include <pthread.h>

#define MAX 10

#define MAX_NTHR 20

#include <stdlib.h>

int recip[MAX];

int art=0;

pthread_t tid[MAX_NTHR];

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t condPut=PTHREAD_COND_INITIALIZER;

pthread_cond_t condGet=PTHREAD_COND_INITIALIZER;

int P=5, C=5;

int p[5],c[5];

int pozPut,pozGet;

//afiseaza starea curenta a producatorilor si a consumatorilor

void scrie() {

int i;

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

printf("P%d_%d ",i,p[i]);

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

printf("C%d_%d ",i,c[i]);

printf("B: ");

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

if (recip[i]!=0)

printf("%d ",recip[i]);

printf("\n");

}

//verifica daca buferul este plin

int plin() {

//pthread_mutex_lock(&mutex);

if (recip[pozPut]!=0)

return 1;

else

return 0;

//pthread_mutex_unlock(&mutex);

}

//verifica daca buferul este gol

int gol() {

Page 46: Programare Multi Threading

Programare multithreading - 46 –

//pthread_mutex_lock(&mutex);

if (recip[pozGet]==0)

return 1;

else

return 0;

//pthread_mutex_unlock(&mutex);

}

//pune un articol in buffer

put(int art,char sind[]) {

int i=atoi(sind);

pthread_mutex_lock(&mutex);

while(plin()) {

p[i]=-art;

pthread_cond_wait(&condPut,&mutex);

}

recip[pozPut]=art;

pozPut=(pozPut+1)%MAX;

p[i]=art;

scrie();

p[i]=0;

pthread_mutex_unlock(&mutex);

pthread_cond_signal(&condGet);

}

//extrage un articol din buffer

get (char sind[]) {

int i=atoi(sind);

pthread_mutex_lock(&mutex);

while(gol()) {

c[i]=-1;

pthread_cond_wait(&condGet,&mutex);

}

c[i]=recip[pozGet];

recip[pozGet]=0;

pozGet=(pozGet+1)%MAX;

scrie();

c[i]=0;

pthread_mutex_unlock(&mutex);

pthread_cond_signal(&condPut);

}

//rutina thread-urilor producator

void* produc(void* sind) {

int sl;

while (1) {

art++;

put(art,sind);

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

}

}

//rutina thread-urilor consumator

void* consum (void* sind) {

int sl;

while (1) {

get(sind);

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

}

}

//functia principala "main"

main() {

Page 47: Programare Multi Threading

Programare multithreading - 47 –

int i;

char *sind;

srand(0);

pozPut=0;pozGet=0;

for (i=0;i<MAX;i++) recip[i]=0;

for (i=0;i<P;i++) {

sprintf(sind,"%d",i);

pthread_create(&tid[i],NULL,produc,sind);

}

for (i=0;i<C;i++) {

sprintf(sind,"%d",i);

pthread_create(&tid[i+P],NULL,consum,sind);

}

for (i=0;i<P+C;i++)

pthread_join(tid[i],NULL);

}

Programul 2.11 Sursa ProducatorConsumator.c

O porţiune din rezultatul execuţiei este:

- - - - - - - - - - - -

P0_0 P1_1 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_0 B: 1

P0_0 P1_0 P2_2 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_0 B: 1 2

P0_0 P1_0 P2_0 P3_3 P4_0 C0_0 C1_0 C2_0 C3_0 C4_0 B: 1 2 3

P0_0 P1_0 P2_0 P3_0 P4_4 C0_0 C1_0 C2_0 C3_0 C4_0 B: 1 2 3 4

P0_5 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_0 B: 1 2 3 4 5

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_1 C2_0 C3_0 C4_0 B: 2 3 4 5

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_2 C3_0 C4_0 B: 3 4 5

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_3 C4_0 B: 4 5

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_4 B: 5

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_5 B:

P0_0 P1_0 P2_0 P3_0 P4_6 C0_0 C1_0 C2_0 C3_0 C4_-1 B: 6

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_6 B:

P0_0 P1_0 P2_0 P3_0 P4_7 C0_0 C1_0 C2_0 C3_0 C4_-1 B: 7

P0_0 P1_0 P2_0 P3_0 P4_8 C0_0 C1_0 C2_0 C3_0 C4_-1 B: 7 8

P0_0 P1_0 P2_0 P3_0 P4_9 C0_0 C1_0 C2_0 C3_0 C4_-1 B: 7 8 9

P0_0 P1_0 P2_0 P3_0 P4_0 C0_0 C1_0 C2_0 C3_0 C4_7 B: 8 9

- - - - - - - - - - - -

2.5.8.3 Problema cititorilor şi a scriitorilor

O soluţie Posix a problemei cititorilor şi scriitorilor, care nu utilizează obiecte reader/writer, este

dată în programul 4.12.

#include <pthread.h>

#define MAX 10

#include <stdlib.h>

#define S 5

#define C 5

int recip[MAX];

int art=0;

pthread_t tid[S+C];

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t condSc=PTHREAD_COND_INITIALIZER;

int s[S],c[C];

int cititori;

//afiseaza starea curenta a cititorilor si scriitorilor

void afiseaza() {

Page 48: Programare Multi Threading

Programare multithreading - 48 –

int i;

int z=0;

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

printf("S%d %d ",i,s[i]);

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

printf("C%d %d ",i,c[i]);

putchar('\n');

}

//scrie un articol in buffer

scrie(char sind[]) {

int i=atoi(sind);

pthread_mutex_lock(&mutex);

while(cititori>0) {

s[i]=-1;

pthread_cond_wait(&condSc,&mutex);

}

s[i]=1;

afiseaza();

s[i]=0;

pthread_mutex_unlock(&mutex);

}

//citeste un articol din buffer

citeste (char sind[]) {

int i=atoi(sind);

c[i]=-1;

pthread_mutex_lock(&mutex);

cititori++;

pthread_mutex_unlock(&mutex);

c[i]=1;

pthread_mutex_lock(&mutex);

afiseaza();

c[i]=0;

cititori--;

pthread_mutex_unlock(&mutex);

pthread_cond_signal(&condSc);

}

//rutina thread cititor

void* cititor(void* sind) {

int sl;

while (1) {

citeste(sind);

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

}

}

//rutina thread scriitor

void* scriitor (void* sind) {

int sl;

while (1) {

scrie(sind);

sl=1+(int) (3.0*rand()/(RAND_MAX+1.0));

sleep(sl);

}

}

//functia principala "main"

main() {

int i;

char *sind;

srand(0);

for (i=0;i<S;i++) {

Page 49: Programare Multi Threading

Programare multithreading - 49 –

sprintf(sind,"%d",i);

pthread_create(&tid[i],NULL,scriitor,sind);

}

for (i=0;i<C;i++) {

sprintf(sind,"%d",i);

pthread_create(&tid[i+S],NULL,cititor,sind);

}

for (i=0;i<S+C;i++)

pthread_join(tid[i],NULL);

}

Programul 2.12 Sursa CititorScriitor.c

O parte din rezultatul execuţiei este:

- - - - - - - -

S0 0 S1 0 S2 0 S3 0 S4 0 C0 0 C1 0 C2 0 C3 0 C4 1

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 0 C0 0 C1 0 C2 0 C3 0 C4 1

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 0 C0 0 C1 0 C2 0 C3 0 C4 1

S0 0 S1 0 S2 0 S3 0 S4 0 C0 0 C1 0 C2 0 C3 0 C4 1

S0 0 S1 0 S2 0 S3 0 S4 -1 C0 0 C1 0 C2 0 C3 0 C4 1

S0 0 S1 0 S2 0 S3 0 S4 -1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 -1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

S0 0 S1 0 S2 0 S3 0 S4 1 C0 0 C1 0 C2 0 C3 0 C4 0

- - - - - - - - - -

2.6 Thread-uri pe platforme Microsoft: Windows NT, 2000

2.6.1 Caracteristici ale thread-urilor sub Windows NT

In 4.1.2 am precizat o clasificare a thread-urilor, în funcţie de implementarea acestora în: thread-

uri nucleu şi thread-uri user. Aceste două tipuri de thread-uri se regăsesc şi pe platformele

Windows, sub denumirea de thread-uri, respectiv fibre sau fibers.

Din punct de vedere al sarcinilor de executat, Win32 [63, 67] mai defineşte două tipuri de

thread-uri: user-interface(UI) şi worker. Thread-urile user-interface au asociate una sau mai

multe ferestre, pentru care sistemul aşteaptă evenimente într-o buclă de mesaje. La sosirea unui

eveniment, este apelată metoda care deserveşte evenimentul / fereastra corespunzătoare.

Sincronizarea thread-urilor UI este realizată de sistem, iar pentru thread-urile worker

sincronizarea este sarcina programatorului. In continuare vom trata numai thread-urile worker.

Sub Windows NT, threadul este cea mai mică entitate executabilă la nivel nucleu. Fiecare proces

conţine unul sau mai multe thread-uri. In momentul creării procesului (vezi 3.1.3.3), odată cu el

se crează threadul primar al procesului. Threadul primar poate crea la rândul său alte thread-uri

cu care va partaja spaţiul de adrese al procesului comun. De asemenea, ele mai partajează şi alte

resurse sistem: descriptori de fişiere, etc.

Page 50: Programare Multi Threading

Programare multithreading - 50 –

Fiecare thread are însă un context propriu, inclusiv stive de execuţie şi date specifice. In cele ce

urmează, sunt prezentate câteva dintre componentele esenţiale ale unui thread în executivul NT:

Un identificator unic, numit client ID.

Contextul threadului, care constă din:

-Conţinutul unui set de regiştri volatili, reprezentând starea procesorului.

-Două stive, una pentru utilizarea în mod utilizator şi cealaltă pentru utilizarea în mod

nucleu.

-O zonă de memorie privată folosită de către subsisteme şi diversele biblioteci dinamice

(DLL-uri).

In documentaţia interfeţei Win32 API, se precizează că un thread are asociate una sau mai multe

fibre (fibers). Fibra este cea mai mică entitate executabilă care este creată şi executată la nivel

utilizator. Gestiunea este realizată exclusiv de programator pentru toate operaţiile: creare,

planficare, distrugere, etc.

Planificarea thread-urilor este sarcina exclusivă a nucleului sistemului de operare şi se bazează

pe prioritatea de bază a thread-urilor.

In cadrul unui proces, thread-urile se execută independent unele de celelalte şi implicit nici unul

nu are cunoştinţă de celălalt. In funcţie de aplicaţia concretă, programatorul poate decide dacă e

nevoie ca thread-urile să se “vadă” între ele.

2.6.2 Operaţii asupra thread-urilor: creare, terminare

2.6.2.1 Crearea unui thread

Prototipul funcţiei de creare este:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,

DWORD dwStackSize,

LPTHREAD_START_ROUTINE lpStartAddress,

LPVOID lpParameter,

DWORD dwCreationFlags,

LPDWORD lpThreadId

);

lpThreadAttributes - pointer la atributele de securitate

dwStackSize - dimensiunea iniţială a stivei threadului, în octeţi

lpStartAddress - pointer la funcţia ce dirijează threadul

lpParameter - argumentul funcţiei

dwCreationFlags - flaguri de creare

lpThreadId - pointer la identificatorul threadului

La crearea threadului, este generat un descriptor care identifică în mod unic threadul în sistem.

După creare, se lansează în execuţie funcţia specificată prin parametrul lpStartAddress.

Această funcţie are parametrii specificaţi prin lpParameter şi întoarce o valoare de tip

DWORD. Pentru a determina valoarea întoarsă de această funcţie, se poate folosi funcţia

GetExistCodeThread().

Page 51: Programare Multi Threading

Programare multithreading - 51 –

Dacă parametrul dwCreationFlags are valoarea 0, atunci threadul se va executa imediat,

altfel el va fi trecut în starea CREATE_SUSPENDED.

Dacă funcţia se execută cu succes, valoarea întoarsă este un handler la threadul nou creat, altfel

funcţia întoarce NULL.

Dacă nu se precizează un descriptor de securitate, handler-ul dispune de drepturi absolute de

acces la threadul nou creat si poate fi folosit în orice funcţie care cere un obiect de tipul

descriptor de thread (de exemplu functiile: suspend(), resume(), terminate()). Pe

de altă parte, dacă este furnizat un parametru de securitate, înainte ca un proces să obţină acces la

un anumit thread prin intermediul descriptorului de thread, se verifică dacă acest acces este

autorizat.

Dacă parametrul lpStartAddress reprezintă o adresă invalidă se generează o excepţie şi

threadul se termină cu eroare.

2.6.2.2 Terminarea unui thread

Un thread îşi încheie execuţia în următoarele condiţii:

la ieşirea din procedura asociată threadului.

la apelul funcţiilor ExitProcess(), ExitThread() apelate din threadul curent.

dacă se apelează ExitProcess() sau TerminateThread() din alte procese, cu

argument handler-ul threadului care urmează a fi distrus sau din alte thread-uri, folosind, de

asemenea, funcţia TerminateThread().

Prototipurile unora dintre funcţiile de terminare a unui thread sunt:

void ExitThread(UINT exitcode);

BOOL TerminateThread(HANDLE hThread, DWORD exitcode);

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD exitcode);

Parametrul hThread identifică threadul care se va termina.

Apelul ExitThread provoacă terminarea threadului curent cu întoarcerea codului de ieşire

exitcode. După apelul funcţiei, stiva asociată threadului este eliberată, iar starea obiectului

thread devine semnalată.

Funcţia GetExitCodeThread primeşte în zona punctată de exitcode codul de ieşire al

threadului indentificat prin hThread.

După terminare, obiectul thread rămâne în memorie până când sunt închişi şi toţi descriptorii

(handle) asociaţi threadului. Inchiderea se face cu apelul CloseHandle, descrisă în 3.2.3.1.

2.6.3 Instrumente standard de sincronizare

Mecanismele de comunicare şi sincronizare între thread-uri sunt furnizate de interfaţa

Win32API, care furnizează primitive de lucru cu evenimente, semafoare, variabile mutex,

secţiuni critice. Aceste obiecte de sincronizare au, aşa cum am mai spus în cazul semafoarelor

Page 52: Programare Multi Threading

Programare multithreading - 52 –

(3.4.2.1) două stări: semnalat şi nesemnalat. Starea semnalat presupune de obicei îndeplinirea

unei condiţii şi semnalarea acestui fapt unor thread-uri interesate.

2.6.3.1 Funcţii de aşteptare

Cea mai simplă modalitate de comunicare este aşteptarea terminării unui thread. Pentru

sincronizare se aşteaptă, de asemenea şi după alte obiecte de sincronizare: evenimente,

semafoare, variabile mutex, etc. Pentru realizarea operaţiilor de aşteptare, atât thread-urile, cât şi

celelalte obiecte de sincronizare sunt privite de NT ca şi obiecte care urmează să ajungă în starea

semnalat.

Funcţiile de aşteptare modifică starea unui obiect de sincronizare astfel:

Dacă obiectul de sincronizare este un semafor, atunci valoarea acestuia este micşorată cu o

unitate şi starea lui este setată ca nesemnalată.

Dacă obiectul este o variabilă mutex, atunci starea ei este setată ca nesemnalată.

Funcţiile Win32 API prin care se realizează aşteptarea au fost deja prezentate în 3.4.2.1. Pentru

scopurile de sincronizare a thread-urilor sunt utile următoarele funcţii:

WaitForSingleObject şi WaitForSingleObjectEx, ambele primind ca parametru

un handle al unui obiect de sincronizare. Ca efect, blochează execuţia threadului curent până

când obiectul dat ca parametru ajunge în starea semnalat. Eventual limitează timpul de

aşteptare la un număr maxim de milisecunde (dacă parametrul este diferit de INFINITE).

SignalObjectAndWait permite threadului apelant ca, în mod atomic, să stabilească

starea setat a unui obiect de sincronizare şi apoi să aştepte ca un alt obiect de sincronizare să

fie semnalat.

WaitForMultipleObjects şi WaitForMultipleObjectsEx permit threadului să

specifice un tablou de handle la obiecte de sincronizare. Funcţiile returnează starea de

semnalat a unuia dintre obiecte sau a tuturora, sau expirarea intervalului de timp destinat

aşteptării.

Pentru detalii asupra acestor funcţii se va revedea 3.4.2.1 sau [63, 83]

2.6.3.2 Variabile mutex

Reamintim că variabilele mutex permit implementarea accesului exclusiv la o resursă partajată

între mai multe thread-uri. Semantica obiectelor de sincronizare mutex este similară cu cea

întâlnită la implementarea thread-urilor de pe platformele Unix.

Crearea unei astfel de variabile se face cu funcţia: HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL binitialOwner,

LPCTSTR lpName

);

lpMutexAttributes - pointer la atributele de securitate

bInitialOwner - flag care specifică dacă variabila mutex este sau nu proprietatea threadului

lpName - pointer la numele variabilei mutex

Page 53: Programare Multi Threading

Programare multithreading - 53 –

In caz de succes, funcţia returnează un handler la variabila mutex, altfel întoarce NULL. Acest

handler poate fi specificat ca parametru în funcţiile de blocare a variabilei mutex, de eliberare a

ei sau de distrugere.

Ocuparea unei variabile mutex se face prin funcţiile de aşteptare prezentate în secţiunea

precedentă. De exemplu apelul WaitForSingleObject(g_hMutex, INFINITE) se va

termina doar când starea variabilei mutex indentificată prin handler-ul g_hMutex devine

semnalată.

Eliberarea unei variabile mutex se realizeaza cu funcţia: BOOL ReleaseMutex(HANDLE hMutex);

hMutex - handler la obiectul mutex

Distrugerea unei variabile mutex se face fie prin invocând CloseHandle(). Dacă acest apel

lipseşte, variabilele mutex sunt eliminate de sistem.

2.6.3.3 Semafoare fără nume

In 3.4.2 am prezentat utilizarea semafoarelor NT pentru sincronizarea proceselor. Aceleaşi

semafoare pot fi, natural, utilizate pentru sincronizarea thread-urilor.

Pentru sincronizarea thread-urilor din cadrul aceluiaşi proces, este de preferată utilizarea

semafoarelor fără nume, deoarece nucleul sistemului este scutit de gestiunea lor. Crearea unui

astfel de semafor trebuie făcută folosind apelul CreateSemaphore, descris în 3.4.2.1. Pentru

a fi semafor anonim trebuie ca ultimul parametru, pointer la numele semaforului, să aibă

valoarea NULL.

Handle-ul întors de funcţia de creare trebuie salvat într-o variabilă care să fie vizibilă de către

toate thread-urile interesate. Acest handle trebuie citat în toate funcţiile de aşteptare

Valoarea semaforului poate fi mărită cu o cantitate pozitivă apelând funcţia

ReleaseSemaphore() descrisă în 3.4.2.1. Ea are ca prim argument handle-ul semaforului şi

cantitatea cu care se măreşte valoarea.

Aşteptarea la semafor se face folosind funcţiile de aşteptare descrise în secţiunea 3.4.2.1.

2.6.3.4 Secţiuni critice

O variabilă de tip secţiune critică se declară astfel: CRITICAL_SECTION numeSectiuneCritica;

Utilizarea secţiunii critice se face astfel:

EnterCriticalSection(&numeSectiuneCritica);

- - - Corpul sectiunii critice - - -

LeaveCriticalSection(&numeSectiuneCritica);

Page 54: Programare Multi Threading

Programare multithreading - 54 –

2.6.3.5 Alte obiecte de sincronizare

In continuare, vom descrie pe scurt alte obiecte de sincronizare, ale căror stări: semnalat,

respectiv nesemnalat, permit să fie folosite ca parametri în apelurile unor funcţii de aşteptare,

respectiv funcţii de semnalizare.

Event

Acest obiect are asociată o anumită condiţie, numită eveniment. La apelul funcţiei

SetEvent() pentru un obiect event, starea acestuia devine semnalată şi thread-urile care

aşteaptă condiţia, sunt notificate.

Timer

Un obiect timer se construieşte folosind funcţia CreateTimer() şi devine semnalat când

intervalul de timp, specificat la creare, expiră.

Change notification

Un astfel de obiect se creează cu ajutorul funcţiei FindFirstChangeNotification() şi

starea obiectului devine semnalată când modificarea specificată la creare a apărut în structura de

directoare.

Console input

Obiectul este construit când se creează o consolă, folosind apelurile CreateFile() şi

GetStdHandle(). Aceste funcţii întorc un handler la un obiect console input. Starea

obiectului devine semnalată când există octeţi necitiţi în bufferul consolei şi nesemnalată când

bufferul este gol.

Process

Acest obiect se creează cu funcţia CreateProcess. Starea sa este setată ca nesemnalată când

procesul se execută şi semnalată, când procesul s-a terminat.

Pentru informaţii suplimentare legate de operaţiile de creare şi semantica acestor obiecte de

sincronizare, se recomandă studiul documentaţiei MSDN [83].

2.6.4 Atributele şi planificarea thread-urilor NT

2.6.4.1 Atributele thread-urilor

Atributele de securitate sunt precizate printr-un obiect de tip SECURITY_ATTRIBUTES în

funcţia de creare a unui thread. Structura acestui tip este:

typedef struct _SECURITY_ATTRIBUTES {

DWORD nLength;

LPVOID lpSecurityDescriptor;

BOOL bInheritHandle;

} SECURITY_ATTRIBUTES;

unde nLength reprezintă dimensiunea în octeţi a structurii, lpSecurityDescriptor este

descriptor de securitate şi precizează cum va fi folosit handler-ul la obiectul creat (în acest caz

obiect thread) în alte funcţii care cer argument handler. Dacă este NULL, se va folosi

Page 55: Programare Multi Threading

Programare multithreading - 55 –

descriptorul de securitate al procesului apelant. Parametrul boolean bInheritHandle

specifică dacă un proces fiu va moşteni handler-ul obiectului nou creat.

Un alt atribut este dimensiunea stivei de executie. Stiva specifică fiecărui thread este alocată

automat la crearea threadului în spaţiul de adrese a procesului. Dacă pentru dimensiune se

specifică valoarea 0, stiva noului thread va avea aceeaşi dimensiune cu cea a threadului primar

2.6.4.2 Priorităţile thread-urilor

Fiecare thread are asociată o anumită prioritate de bază, care combină prioritatea clasei

procesului şi nivelul de prioritate al threadului în cadrul clasei respective.

Pentru a permite şi altor thread-uri să ruleze, sistemul poate mări sau micşora prioritatea unui

thread, folosind funcţiile SetPriorityClass() şi SetThreadPriority().

Prototipurile acestora sunt:

BOOL SetPriorityClass(

HANDLE hProcess, //handler-ul procesului

DWORD dwPriorityClass //prioritatea procesului

);

BOOL SetThreadPriority(

HANDLE hThread, //handler-ul threadului

int nPriority //prioritatea threadului

);

Funcţia care întoarce prioritatea clasei este getPriorityClass() cu prototipul:

DWORD GetPriorityClass(HANDLE hProcess);

In caz de eroare, funcţia întoarce valoarea 0.

Prioritatea threadului se obţine prin apelul GetThreadPriority() cu prototipul:

int GetThreadPriority(HANDLE hThread);

In caz de eroare este întors codul THREAD_PRIORITY_ERROR_RETURN.

Pentru procese, există patru clase de priorităţi:

HIGH_PRIORITY_CLASS –indică un proces care execută operaţii critice din punct de

vedere al timpului. Aceste operaţii trebuie să ruleze imediat pentru ca procesul să se execute

corect. Un exemplu de astfel de operaţie este obţinerea listei de procese din sistem, operaţie

care trebuie să furnizeze răspunsul imediat, indiferent de încărcarea procesorului (sau a

procesoarelor).

IDLE_PRIORITY_CLASS –pentru procese care pot să ruleze doar când sistemul este

disponibil. Exemple de procese cu această prioritate sunt programele de tip screen saver.

NORMAL_PRIORITY_CLASS –indică procesare normală fără reguli speciale de planificare.

REALTIME_PRIORITY_CLASS –cea mai mare prioritate posibilă. Un proces cu această

prioritate rulează înaintea tuturor proceselor din sistem.

Componenta de planificare a sistemului menţine o coada de thread-uri pentru fiecare nivel de

prioritate. Thread-urile cu prioritate superioară rulează înaintea celor cu prioritate mai mică, iar

Page 56: Programare Multi Threading

Programare multithreading - 56 –

pentru acelaşi nivel, thread-urile sunt planificate după o politică de tip round-robin. Există nivele

de prioritate de la 0 (cea mai mică) la 31 (cea mai mare).

Prioritatea relativă a unui thread poate avea următoarele valori:

THREAD_PRIORITY_ABOVE_NORMAL –indică o valoare cu 1 mai mare decât

prioritatea normală pentru prioritatea clasei.

THREAD_PRIORITY_BELOW_NORMAL –indică o valoare cu 1 mai mică decât

prioritatea normală pentru prioritatea clasei.

THREAD_PRIORITY_HIGHEST –indică o valoare cu 2 mai mare decât prioritatea normală

pentru prioritatea clasei.

THREAD_PRIORITY_IDLE –indică o prioritate de nivel 1 pentru clasele

IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS şi HIGH_PRIORITY_CLASS

şi o prioritate de nivel 16 pentru REALTIME_PRIORITY_CLASS.

THREAD_PRIORITY_LOWEST -indică o valoare cu 2 mai mică decat prioritatea normală

pentru prioritatea clasei.

THREAD_PRIORITY_NORMAL –indică o prioritate normală pentru prioritatea clasei.

THREAD_PRIORITY_TIME_CRITICAL –indică o prioritate de nivel 15 pentru clasele

IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS şi HIGH_PRIORITY_CLASS

şi o prioritate de nivel 31 pentru REALTIME_PRIORITY_CLASS.

Implicit, threadul este creat cu prioritatea THREAD_PRIORITY_NORMAL.