Programare concurentă

 pe platforme Unix, Windows, Java

 

 

Florian Mircea Boian

Corina Maria Ferdean

Rareş Florin Boian

Radu Călin Dragoş

 

 

CUPRINS

 

0        Introducere   10

1        Nivelele prelucrărilor concurente   12

1.1            Procesări paralele şi clasificarea Flynn  12

1.1.1                Maşini SISD   12

1.1.2                Maşini SIMD   13

1.1.3                Maşini MISD   13

1.1.4                Maşini MIMD   13

1.2            Granularităţi ale paralelismului / concurenţei 14

1.3            Tehnica pipeline  15

1.4            Procesoare vectoriale şi sisteme cluster  19

1.4.1                Procesoare vectoriale  19

1.4.2                Sisteme cluster 21

1.5            Paralelism şi concurenţă la nivelul sistemului de operare  22

1.6            Evaluare multiprocesor a expresiilor complexe  24

1.7            Reorganizarea succesiunilor de atribuiri 26

1.8            Paralelizare la nivel de cicluri for  29

2        Concepte abstracte utilizate în descrierea concurenţei 32

2.1            Paradigme de programare nesecvenţială  32

2.1.1                Programare paralelă  33

2.1.2                Programare concurentă  35

2.1.3                Programare distribuită  37

2.2            Relaţia procese - thread-uri 41

2.2.1                Definirea procesului 41

2.2.2                Reprezentarea în memorie a unui proces  42

2.2.3                Definiţia threadului 44

2.3            Scheme de specificare a programelor concurente  45

2.3.1                Grafuri de precedenţă  46

2.3.2                Specificare FORK-JOIN-QUIT  46

2.3.3                Specificare PARBEGIN-PAREND   47

2.4       Situaţii de excepţie generate de concurenţă  49

2.4.1                Rezultate inconsistente  49

2.4.2                Infometare  51

2.4.3                Impas  51

2.5            Mecanisme de control al concurenţei, comunicare şi sincronizare  53

2.5.1                Semafoare  53

2.5.2                Variabile mutex  55

2.5.3                Variabile condiţionale  55

2.5.4                Conceptul de monitor 56

2.5.5                Secţiune şi resursă critică; excludere mutuală  57

2.5.6                Regiuni critice condiţionale  58

2.5.7                Conceptul de întâlnire (rendez-vouz) 59

2.5.8                Limbajul CSP (Communicating Sequential Processes) 59

2.6            Mecanisme de control asincron sau parţial sincron  60

2.6.1                Schimburi de mesaje  61

2.6.2                Fluxuri de octeţi 62

2.6.3                Memorie partajată  66

2.6.4                Evenimente, excepţii, semnale  67

2.7            Probleme specifice care se rezolvă cu ajutorul concurenţei 69

2.7.1                Abordare concurentă în pachete software standard  70

2.7.2                Problema producătorilor şi a consumatorilor 72

2.7.3                Problema cititorilor şi a scriitorilor 74

2.7.4                Problema celor 5 filozofi 75

2.7.5                Problema frizerului 77

2.7.6                Înmulţirea a două matrice  78

3        Programare concurentă la nivel de proces   79

3.1            Procese Unix, Windows, Java  79

3.1.1                Procese sub Unix  79

3.1.1.1                Structura unui proces Unix  79

3.1.1.2                Crearea proceselor UNIX. Apelul sistem fork()   81

3.1.1.3                Apelurile sistem exit, wait şi waitpid   83

3.1.1.4                Execuţia unui program extern; apelurile sistem exec()   84

3.1.2                Un contraexemplu remarcabil 86

3.1.3                Procese sub Windows NT  87

3.1.3.1                Convenţii de limbaj la programarea Windows  87

3.1.3.2                Aplicaţii consolă  89

3.1.3.3                Crearea unui proces Windows  91

3.1.3.4                Terminarea unui proces Windows  92

3.1.3.5                Exemplu de creare proces sub Windows  93

3.1.4                Procese externe lansate din Java  95

3.2            Comunicarea prin pipe între procese  97

3.2.1                Enunţurile unor probleme folosite în exemplele de comunicare  97

3.2.1.1                Semnătură temporală  97

3.2.1.2                Tipărirea de propoziţii una pe linie  100

3.2.2                Pipe, popen şi FIFO sub Unix  104

3.2.2.1                Pipe sub Unix  104

3.2.2.2                Popen  108

3.2.2.3                FIFO – pipe cu nume  110

3.2.3                Pipe sub Windows NT  116

3.2.3.1                Pipe anonim Windows  116

3.2.3.2                Pipe cu nume sub windows  119

3.2.4                Pipe pe platforme Java  123

3.3            Comunicarea între procese folosind mecanismul de memorie partajată  125

3.3.1                Problema foii de calcul rezolvată prin memorie partajată  125

3.3.2                Memorie partajată sub Unix  126

3.3.2.1                Chei şi drepturi de acces  126

3.3.2.2                Principii, structuri şi limite  127

3.3.2.3                Creare, deschidere, ataşare, control 127

3.3.2.4                Implementarea foii de calcul sub Unix  129

3.3.3                Comunicarea între procese Windows prin memorie partajată  131

3.3.3.1                Fişiere I/O mapate în memorie şi zone partajate  131

3.3.3.2                Implementarea foii de calculsub Windows  134

3.4            Sincronizarea proceselor folosind semafoare  136

3.4.1                Semafoare Unix  136

3.4.1.1                Structura setului de semafoare Unix  137

3.4.1.2                Crearea unui set şi accesul la un set existent 138

3.4.1.3                Operaţii asupra semafoarelor Unix  139

3.4.1.4                Controlul seturilor de semafoare  140

3.4.1.5                Exemple de utilizare a semafoarelor Unix  140

3.4.2                Sincronizarea proceselor prin semafoare Windows  145

3.4.2.1                Semafoare cu nume şi anonime; operaţii 145

3.4.2.2                Exemple de utilizare a semafoarelor Windows  149

3.5            Comunicarea prin cozi de mesaje  151

3.5.1        O problemă cu schimburi de mesaje  151

3.5.2                Comunicarea între procese Unix prin cozi de mesaje  152

3.5.2.1                Structura unei cozi de mesaje  152

3.5.2.2                Acces şi operaţii asupra cozii 153

3.5.2.3                Exemple de utilizare a cozilor de mesaje. 155

3.5.3                Comunicarea între procese Windows prin Mailslot 158

3.5.3.1                Arhitectura mailslot 158

3.5.3.2                Programarea folosind mailslot 159

3.5.3.3                Exemplu de lucru cu mailslot 160

3.5.4                Java Message Service (JMS) 163

3.5.4.1                Privire generală  163

3.5.4.2                Arhitectura API JMS  163

3.5.4.3                Domenii de mesaje  164

3.5.4.4                Modelul de programare API al JMS  166

3.5.4.5                Pregătirea pentru rularea de programe JMS  172

3.5.4.6                Exemple de aplicaţii punct la punct  şi publicare / înscriere  173

4        Programare concurentă la nivel de thread-uri 177

4.1            Caracteristici generale  177

4.1.1                Contextul şi stările unui thread  177

4.1.2                Tipuri de thread-uri 178

4.1.2.1                O clasificare a thread-urilor 178

4.1.2.2                Thread-uri nucleu  180

4.1.2.3                Thread-uri utilizator 180

4.1.2.4                Utilizări combinate de thread-uri 181

4.1.3                Modele de legare thread-uri user - thread-uri nucleu  181

4.1.3.1                Modelul Mx1  182

4.1.3.2                Modelul 1x1  183

4.1.3.3                Modelul MxN   184

4.1.4                Avantaje şi dezavantaje în utilizarea thread-urilor 184

4.2            Exemple de probleme rezolvabile prin thread-uri 186

4.2.1                Adunarea în paralel a n numere  186

4.2.2                Problema producătorului şi consumatorului 189

4.2.3                Problema cititorilor şi a scriitorilor 190

4.3            Thread-uri pe platforme Unix: Posix şi Solaris  191

4.3.1                Caracteristici şi comparaţii Posix şi Solaris  191

4.3.1.1                Contextul şi stările unui thread  192

4.3.1.2                Caracteristici ale thread-urilor sub Linux  192

4.3.1.3                Caracteristici ale thread-urilor sub Solaris  193

4.3.1.4                Similarităţi şi facilităţi specifice  194

4.3.2                Operaţii asupra thread-urilor: creare, terminare  194

4.3.2.1                Crearea unui thread  194

4.3.2.2                Terminarea unui thread  195

4.3.2.3                Aşteptarea terminării unui thread  197

4.3.2.4                Un prim exemplu  197

4.3.3                Instrumente standard de sincronizare  200

4.3.3.1                Operaţii cu variabile mutex  200

4.3.3.2                Operaţii cu variabile condiţionale  202

4.3.3.3                Operaţii cu semafoare  206

4.3.3.4                Blocare de tip cititor / scriitor (reader / writer) 207

4.3.4                Exemple de sincronizări 208

4.3.5                Obiecte purtătoare de atribute Posix  211

4.3.5.1                Iniţializarea şi distrugerea unui obiect atribut 212

4.3.5.2                Gestiunea obiectelor purtătoare de atribute thread  212

4.3.5.3                Gestiunea obiectelor purtătoare de atribute mutex  213

4.3.5.4                Gestiunea obiectelor purtătoare de atribute pentru variabile condiţionale  214

4.3.5.5                Purtătoare de atribute pentru obiecte partajabile reader / writer 214

4.3.6                Planificarea thread-urilor sub Unix  215

4.3.6.1                Gestiunea priorităţilor sub Posix  215

4.3.6.2                Gestiunea priorităţilor sub Solaris  216

4.3.6.3                Exemplu de planificare manuală a thread-urilor pe Solaris  216

4.3.7                Facilităţi speciale ale lucrului cu thread-uri Unix  217

4.3.7.1                Execuţie cel mult o dată  217

4.3.7.2                Asociere date specifice thread-urilor cu chei 217

4.3.7.3                Operaţii specifice lwp sub Solaris  220

4.3.8                Exemple clasice de lucru cu thread-uri 223

4.3.8.1                Adunarea în paralel a n numere  223

4.3.8.2                Problema producătorilor şi a consumatorilor 225

4.3.8.3                Problema cititorilor şi a scriitorilor 227

4.4            Thread-uri pe platforme Microsoft: Windows NT, 2000  229

4.4.1                Caracteristici ale thread-urilor sub Windows NT  229

4.4.2                Operaţii asupra thread-urilor: creare, terminare  230

4.4.2.1                Crearea unui thread  230

4.4.2.2                Terminarea unui thread  231

4.4.3                Instrumente standard de sincronizare  232

4.4.3.1                Funcţii de aşteptare  232

4.4.3.2                Variabile mutex  233

4.4.3.3                Semafoare fără nume  233

4.4.3.4                Secţiuni critice  234

4.4.3.5                Alte obiecte de sincronizare  234

4.4.4                Atributele şi planificarea thread-urilor NT  235

4.4.4.1                Atributele thread-urilor 235

4.4.4.2                Priorităţile thread-urilor 235

4.4.5                Facilităţi speciale ale lucrului cu thread-uri NT  237

4.4.5.1                Date specifice thread-urilor 237

4.4.5.2                Thread-uri utilizator: fibre NT  238

4.4.5.3                Utilizarea thread-urilor în regim multiprocesor 239

4.4.6                Exemple clasice de lucru cu thread-uri 240

4.4.6.1                Adunarea în paralel a n numere  241

4.4.6.2                Problema producătorilor şi consumatorilor 242

4.4.6.3                Problema cititorilor şi scriitorilor 245

4.5            Thread-uri Java  248

4.5.1                Elemente de limbaj Java în contextul thread-urilor 248

4.5.1.1                Obiecte versus thread-uri 248

4.5.1.2                Clasa Thread   250

4.5.1.3                Interfaţa Runnable   252

4.5.1.4                Grupuri de thread-uri 253

4.5.2                Caracteristici ale Java Threading API 254

4.5.2.1                Tipuri de thread-uri Java; particularităţi ale diverselor implementări 254

4.5.2.2                Stările unui thread Java  257

4.5.3                Operaţii asupra thread-urilor Java; creare, terminare  259

4.5.3.1                Crearea unui thread  259

4.5.3.2                Terminarea unui thread  261

4.5.3.3                Aşteptarea terminării unui thread  261

4.5.3.4                Exemple de creare / terminare thread-uri 261

4.5.4                Sincronizarea thread-urilor Java  264

4.5.4.1                Conceptul de monitor Java; synchronized   264

4.5.4.2                Mecanismul wait / notify   265

4.5.4.3                O schemă exemplu de sincronizări 267

4.5.5                Exemple clasice de lucru cu thread-uri 268

4.5.5.1                Adunarea în paralel a n numere  268

4.5.5.2                Problema producătorului şi consumatorului 270

4.5.5.3                Problema cititorilor şi a scriitorilor 272

4.5.6                Multithreading folosind JNI 275

5        Aplicaţii concurente complexe   279

5.1 Scheme de proiectare a programelor concurente  279

5.1.1                Modelul boss/worker 280

5.1.2                Modelul peer 281

5.1.3                Modelul pipelining  281

5.2            Implementarea threadurilor NT în MFC   282

5.2.1                Creare şi terminare threaduri 283

5.2.1.1                Crearea thread-urilor folosind mecanismul MFC   283

5.2.1.2                Crearea thread-urilor user-interface  283

5.2.1.3                Crearea thread-urilor worker 284

5.2.1.4                Terminarea thread-urilor 284

5.2.2                Comunicarea şi sincronizarea thread-urilor MFC   285

5.2.3                Exemple  286

5.2.3.1                Un exemplu de thread user-interface  286

5.2.3.2                Un exemplu de thread worker 289

5.3            Utilizări combinate: threaduri, procese Unix, semnale  292

5.3.1                Thread-uri şi procese  292

5.3.1.1                Trei threaduri şi un proces fiu  292

5.3.1.2                Procese fii create din threaduri 293

5.3.1.3                Utilizarea funcţiei clone   294

5.3.2                Thread-uri Posix şi semnale Unix  296

5.3.2.1                Consideraţii generale  296

5.3.2.2                Exemple cu semnale livrate procesului de bază  298

5.3.2.3                Exemplu cu semnal livrat unui thread particular 300

5.4            Utilizarea thread-urilor în appleturi şi servleturi Java  302

5.4.1                Thread-uri în applet-uri Java  302

5.4.2                Thread-uri în servlet-uri Java  303

5.5            Aplicaţie vizuală multi-threading sub NT   305

5.6       Server Java concurent pentru chat 310

5.7       Client FTP noninteractiv  318

5.7.1                Protocolul FTP (File Transfer Protocol) 318

5.7.2                Aplicatia “Client FTP”  319

5.8            Evaluarea unor performanţe ale programelor cu thread-uri 324

5.8.1                Vizualizarea datelor volumetrice  325

5.8.1.1                Date volumetrice  325

5.8.1.2                Ray casting  325

5.8.1.3                Aspecte cantitative  326

5.8.2                Detalii de implementare  327

5.8.2.1                Iniţializare  327

5.8.2.2                Generarea imaginilor 329

5.8.3                Studii de performanţă  330

5.8.3.1                Iniţializări pe diverse platforme  330

5.8.3.2                Generarea imaginilor pe diverse platforme  332

5.8.3.3                Generarea folosind diverse politici de planificare  334

5.8.4                Concluzii privitoare la performanţe  336

6      Index   337

7      Lista de figuri 339

8      Lista de programe   342

9        Bibliografie   345

10        Text coperta spate   352

 

 

0       Introducere

 

Nu mai este o noutate faptul că domeniul informaticii este extrem de dinamic. Acest dinamism se materializează atât prin diversitatea şi complexitatea noilor aplicaţii, cât şi prin implementarea în pachete software a conceptelor şi a modelor clasice de programare. Paradigma programării concurente se găseşte la intersecţia dintre “clasic” şi “modern”. Astfel, primele aspecte legate de concurenţă au apărut în jurul anilor ’50 în cadrul sistemelor de operare, care ofereau facilităţi de multiprogramare. Apariţia calculatoarelor paralele şi a reţelelor de calculatoare a constituit cadrul fizic adecvat pentru utilizarea facilităţilor de concurenţă în cadrul sistemelor de operare, în mod transparent pentru clienţi.

 

In ultimii ani, suportul pentru concurenţă din cadrul sistemului de operare a fost extins la nivelul aplicaţiilor program. Aceasta se realizează, de exemplu, prin intermediul bibliotecilor utilizator sau prin apeluri de interfaţă la funcţionalităţile de concurenţă din sistem. Astfel, utilizatorii pot crea şi gestiona în programele proprii entităţi de execuţie care să ruleze simultan.

 

Lucrarea de faţă îşi propune să prezinte modalitaţile de realizare –la nivelul sistemului de operare- şi utilizare –la nivelul aplicaţiilor program- a suportului pentru implementarea unor aplicaţii concurente. Prezentarea este organizată comparativ, pe sistemele de operare Unix şi Windows, şi pe platforme Java. De ce această abordare? In primul rând pentru că sunt platformele cele mai răspândite astăzi în lume. In al doilea rând, pentru că fiecare dintre ele îşi implementează mecanisme proprii de concurenţă, definite în mod specific, dar cu respectarea unor cerinţe generale de concurenţă. In al treilea rând pentru că se relevă unele diferenţe de utilizare a programării concurente procedurale (de exemplu, în C sub Unix şi Windows) faţă de programarea concurentă orientată obiect (oferită de limbajul Java).

 

Capitolul 1 conţine o descriere succintă a evoluţiei prelucrărilor concurente, pornind de la nivelul hardware –calculatoare paralele, vectoriale, sisteme cluster, tehnica pipeline-, şi apoi în cadrul sistemelor de operare. Sunt descrise de asemenea modalităţi de paralelizare a expresiilor aritmetice sau a ciclurilor for din programele sursă.

 

Capitolul 2 prezintă, într-o manieră semi-formală, conceptele de bază necesare pentru descrierea, specificarea şi implementarea mecanismelor de concurenţă. Tot în acest capitol sunt enumerate si specificate pe scurt câteva aplicaţii concurente uzuale şi probleme specifice care se rezolvă cu ajutorul concurenţei.

 

Capitolul 3 detaliează noţiunea de proces. Este descrisă structura proceselor, precum şi operaţiile de creare, comnunicare şi control ale acestora pe platformele Unix, Windows şi Java.. Ca şi mecanisme de comunicare între procese sunt descrise comunicaţiile prin pipe, prin memorie partajată, prin semafoare şi prin cozi de mesaje.

 

Capitolul 4 abordează conceptul de thread (fir de execuţie) folosit ca entitate de bază pentru modelarea concurenţei, alternativă la procese. Capitolul debutează cu o prezentare generală a thread-urilor, exemple de probleme care se pretează la o astfel de abordare, avantaje şi dezavantaje. In continuare sunt particularizate caracteristicile şi modalităţile de utilizare ale thread-urilor sub Unix (comparativ variantele Posix şi Solaris), sub Windows şi în Java.

 

Capitolul 5 ilustrează utilizarea thread-urilor în combinaţie cu alte concepte şi servicii sistem şi/sau utilizator. Mai întâi se face o scurtă descriere a principalelor scheme de proiectare a programelor concurente: boss/worker, peer, pipelining. Urmează tratarea thread-urilor încapsulate în clase MFC, precum şi utilizarea de threaduri şi semnale sub Unix. Sunt apoi prezentate facilităţile de lucru cu procese şi thread-uri în aplicaţii cu un grad mai mare de complexitate: server Java de chat, client FTP concurent şi neinteractiv, vizualizarea datelor volumetrice. Capitolul se încheie cu un studiu privind performanţele aplicaţiilor cu thread-uri.

 

In lucrare apar foarte multe exemple. Printre cele mai importante amintim: problema producătorilor şi a consumatorilor, problema cititorilor şi a scriitorilor, însumarea în paralel a unui şir de numere, acces concurent la foi de calcul, simulări de acces sincronizat şi nesincronizat la resurse partajabile etc. Exemplele sunt implementate complet în mai multe variante, în funcţie de platformă şi de elementele de sincronizare folosite. Pentru fiecare implementare sunt prezentate programele complete scrise în C, C++ sau Java. Programele sursă sunt numerotate separat în cadrul fiecărui capitol, sub forma: Programul c.n. Intr-o anexă este prezentată lista acestor programe. Autorii au creat o arhivă cu toate sursele programelor descries în lucrare. Doritorii pot să obţină această arhivă apelând anonymous-FTP de la adresa: ftp.ubbcluj.ro. Elementele grafice care însoţesc prezentarea textului sunt numerotate în cadrul fiecărui capitol, începând de la 1, sub forma: Figura c.n. Este anexată o listă a acestor figuri.

 

Lucrarea se adresează în primul rând informaticienilor: studenţi, programatori, data designeri, project manageri etc. Este de asemenea utilă celor interesaţi să pătrundă în domeniul informaticii neconvenţionale, care include paradigma programării concurente.

 

Primul autor este profesor la Departamentul de Informatică, de la Facultatea de Matematică şi Informatică a Universităţii “Babes-Bolyai” Cluj. Ceilalţi trei autori au absolvit secţia Informatică de la aceeaşi facultate, în anii: 2000, 1998, 1999. Toţi trei au absolvit secţia de studii aprofundate “Informatică Distribuită”, în anii: 2001, 1999, 2000. In prezent, toţi trei îşi pregătesc tezele de doctorat în România/Franţa, SUA, Irlanda.

 

Autorii mulţumesc tuturor celor care i-au sprijinit într-un fel sau altul la elaborarea prezentei lucrări. Sunt de asemenea recunoscători tuturor cititorilor care le vor adresa sugestii, observaţii sau întrebări legate de lucrare. Adresele e-mail de contact sunt: florin@cs.ubbcluj.ro, cori@cs.ubbcluj.ro, rares@cs.ubbcluj.ro, bradu@cs.ubbcluj.ro.

 

Cluj-Napoca, octombrie 2001                                        Autorii