Académique Documents
Professionnel Documents
Culture Documents
6.1 Introduction
Considrez par exemple, le fonctionnement dun spool dimpression montr sur la gure 6.1, qui peut tre dlicat. Processus A Rpertoire de spoole Processus B F IG . 6.1 Spool dune imprimante. Quand un processus veut imprimer un chier, il doit placer le nom de ce chier dans un rpertoire spcial, appel rpertoire de spool. Un autre processus, le dmon dimpression, vrie priodiquement sil y a des chiers imprimer. Si cest le cas, il les imprime et retire leur nom du rpertoire. Supposons que : Le rpertoire dimpression ait un trs grand nombre demplacements, numrots 0,1,2,...,N. Chaque emplacement peut contenir le nom dun chier imprimer. 1 Dmon
Tous les processus partagent la variable in qui pointe sur le prochain emplacement libre du rpertoire. Deux processus A et B veulent placer chacun un chier dans la le dimpression et que la valeur de in est 7. Le processus A place son chier la position in qui vaut 7. Une interruption dhorloge arrive immdiatement aprs et le processus A est suspendu pour laisser place au processus B. Ce dernier place galement son chier la position in qui vaut toujours 7 et met jour in qui prend la valeur 8. Il efface ainsi le nom du chier plac par le processus A. Lorsque le processus A sera relanc, il met jour la valeur de in qui prend la valeur 9. Sous ces conditions, deux problmes peuvent se prsenter : Le chier du processus A ne sera jamais imprim ; in ne pointe plus sur le prochain emplacement libre. Comment viter ces problmes ? le problme provient du fait que le processus B a utilis une variable partage avant que A ait ni de sen servir. Si lon arrive bien synchroniser les processus, le problme peut tre rsolu. Lorsquun processus accde la variable in, les autres processus ne doivent ni la lire, ni la modier jusqu ce que le processus ait termin de la mettre jour. Dune manire gnrale, il faut empcher les autres processus daccder un objet partag si cet objet est en train dtre utilis par un processus, ce quon appelle dassurer lexclusion mutuelle. Les situations de ce type, o deux ou plusieurs processus lisent ou crivent des donnes partages et o le rsultat dpend de lordonnancement des processus, sont qualies daccs concurrents.
Lecture de la variable in. Linsertion dun nom de fichier dans le rpertoire. La mise jour de in. Imaginez maintenant quon dsire effectuer une somme des premiers nombres en utilisant des processus lgers. Une vision simpliste pour rsoudre ce problme, pourrait suggrer de diviser le travail en deux processus : le premier qui va calculer S1=1+2+...+M avec M=N % 2 et un deuxime qui additionne S2=M+(M+1)+...+N, et nalement la variable somme_totale=S1 + S2 fournira la rponse. La gure 6.2 illustre cette situation. Un code possible pour rsoudre ce problme est :
ni 51 nf 100
somme_total S1 +S2
F IG . 6.2 Somme de
int somme_totale = 0; void somme_partielle(int ni, int nf) { int j = 0; int somme_partielle = 0; for (j = ni; j <= nf; j++) somme_partielle += j; somme_totale += somme_partielle; pthread_exit(0); }
Mais, avec cet algorithme, si plusieurs processus excutent concurremment ce code, on peut obtenir un rsultat incorrect. La solution correcte ncessite lutilisation des sections critiques.
Le problme de conit daccs serait rsolu, si on pouvait assurer que deux processus ne soient jamais en section critique en mme temps au moyen de lexclusion mutuelle). En gnral, les processus qui excutent des sections critiques sont structurs comme suit : Section non critique. Demande dentre en section critique. Section critique. Demande de sortie de la section critique. Section non critique. Quatre conditions sont ncessaires pour raliser correctement une exclusion mutuelle : 1. Deux processus ne peuvent tre en mme temps en section critique. 2. Aucune hypothse ne doit tre faite sur les vitesses relatives des processus et sur le nombre de processeurs. 3. Aucun processus suspendu en dehors dune section critique ne doit bloquer les autres processus. 4. Aucun processus ne doit attendre trop longtemps avant dentrer en section critique. Comment assurer alors une coopration correcte et efcace des processus ?
Problmes Cette mthode nassure pas lexclusion mutuelle : Supposons quun processus est suspendu juste aprs avoir lu la valeur du verrou qui est gal 0. Ensuite, un autre processus est lu. Ce dernier teste le verrou qui est toujours gal 0, met verrou 1 et entre dans sa section critique. Ce processus est suspendu avant de quitter la section critique. Le premier processus est alors ractiv, il entre dans sa section critique et met le verrou 1. Les deux processus sont en mme temps en section critique. Ainsi cet algorithme nest pas correct. On voit ainsi quil est indispensable dans lnonc du problme de prciser quelles sont les actions atomiques, cest--dire celles qui sont non divisibles. Si on possde sous forme hardware une instruction test and set qui sans tre interrompue lit une variable, vrie si elle est gale 0 et dans ce cas transforme sa valeur en 1, alors il est facile de rsoudre le problme de lexclusion mutuelle en utilisant un verrou. Toutefois on na pas toujours disponible ce type dinstruction, et il faut alors trouver une solution logicielle.
6.4.2 Lalternance
Une autre proposition consiste utiliser une variable tour qui mmorise le tour du processus qui doit entrer en section critique. tour est initialise 0.
// Processus P1 while (1) { // attente active while (tour !=0) ; Section_critique() ; tour = 1 ; Section_noncritique() ; ... } // Processus P2 while (1) { // attente active while (tour !=1) ; Section_critique() ; tour = 0 ; Section_noncritique() ; ... }
Supposons maintenant que le processus P1 lit la valeur de tour qui vaut 0 et entre dans sa section critique. Il est suspendu et P2 est excut. P2 teste la valeur de tour qui est toujours gale 0. Il entre donc dans une boucle en attendant que tour prenne la valeur 1. Il est suspendu et P1 est lu de nouveau. P1 quitte sa section critique, met tour 1 et entame sa section non critique. Il est suspendu et P2 est excut. P2 excute rapidement sa section critique, tour = 0 et sa section non critique. Il teste tour qui vaut 0. Il attend que tour prenne la valeur 1. Problme : On peut vrier assez facilement que deux processus ne peuvent entrer en section critique en mme temps, toutefois le problme nest pas vraiment rsolu car il est possible quun des deux processus ait plus souvent besoin dentrer en section critique que lautre ; lalgorithme lui fera attendre son tour bien que la section critique ne soit pas utilise. Un processus peut tre bloqu par un processus qui nest pas en section critique.
// Nb . p r o c e s s u s // Le t o u r du p r o c e s s u s // 0 i n i t i a l e
10
void e n t r e r _ r e g i o n ( i n t p r o c e s s u s ) // p r o c e s s u s 0 ou 1 { int autre ; // Autre p r o c e s s u s autre = 1 processus ; i n t e r e s s e [ p r o c e s s u s ] = TRUE ; // On e s t i n t e r e s s t o u r = p r o c e s s u s ; // P o s i t i o n e r l e drapeau // Attendre while ( t o u r = = p r o c e s s u s && i n t e r e s s e [ a u t r e ] = =TRUE ) ; } void q u i t t e r _ r e g i o n ( i n t p r o c e s s u s ) { // P r o c e s s u s q u i t e r e g i o n c r i t i q u e i n t e r e s s e [ p r o c e s s u s ] = FALSE ; }
20
Prouver la correction de cet algorithme est quivalent montrer le fait que deux processus ne peuvent pas entrer en section critique en mme temps. Pour y arriver, nous devons rcrire le code, comme montr dans peterson2.c :
8
# define N 2
int tour ; i n t i n t e r e s s e [N ] ;
10
void E n t r e r S o r t i r R e g i o n ( p r o c e s s u s ) { autre = 1 processus ; i n t e r e s s e [ p r o c e s s u s ] = TRUE ; tour = processus ; while ( ( t o u r = = i ) & & i n t e r e s s e [ a u t r e ] ) ; Section_critique ( ) ; i n t e r e s s e [ p r o c e s s u s ] = FALSE ; }
Pour vrier que lalgorithme fonctionne correctement et donne accs aux deux processus la rgion critique, il faut dcomposer cet algorithme en actions atomiques. Voici celles pour le processus P0 :
1. 2. 3. 4. 5. 6. 7. 8. 9. crire (true, interesse[0]); crire (0, tour); a0 = Lire (interesse[1]); Si (!a0) aller 7; t0 = Lire(tour); Si (t0 == 0) aller 3; Section_critique(); crire (false, interesse[0]); Aller 1;
Il faut examiner tous les enchevtrements possibles des deux suites dactions atomiques. On peut pour des raisons de symtrie supposer que le processus P0 excute sa premire instruction avant P1. Il faut toutefois examiner plusieurs cas par la suite :
Cas 1 : P0 excute 3 avant que P1 nexcute 1 les valeurs obtenues par les deux processus sont alors : a0 = false, t0 = 0, a1 = true, t1 = 1 ; Dans ce cas on vrie que P0 entre en section critique, P1 reste bloqu. Cas 2 : P1 excute 1 avant que P0 nexcute 3, et 2 aprs que P0 ait excut 5 a0 = true, t0 = 0, a1 = true , t1 = 1 ; On constate quaucun des deux processus ne rentre dans la section critique, toutefois immdiatement aprs le processus P0 obtient : a0 = true, t0 = 1, ainsi P0 entre en section critique, P1 bloqu Cas 3 : P1 excute 1 avant que P0 nexcute 3, et 2 entre 2 et 5. On a dans ce cas : a0 = true, t0 = 1, a1 = true , t1 = 1 ; ainsi P0 entre en section critique, P1 reste bloqu Cas 4 : P1 excute 1 avant que P0 nexcute 2, et 2 avant 2, donc a0 = true, t0 = 0, a1 = true , t1 = 0 ; P1 entre en section critique et P0 reste bloqu Ayant dmontr la correction de lalgorithme, il faut aussi se poser la question de savoir si deux processus ne peuvent rester bloqus indniment en attendant la section critique, on dirait alors quil y a interblocage. Des raisonnements analogues ceux faits pour la correction montrent quil nen est rien. Cependant, lalgorithme marche seulement pour deux processus.
Linstruction TSL excute en un seul cycle, de manire indivisible, le chargement dun mot mmoire dans un registre et le rangement dune valeur non nulle ladresse du mot charg. Lorsquun processeur excute linstruction TSL, il verrouille le bus de donnes pour empcher les autres processeurs daccder la mmoire pendant la dure de lopration. Cette instruction peut tre utilise pour tablir et supprimer des verrous (assurer lexclusion mutuelle).
10
entrer_region Label : tsl registre, verrou # # cmp registre, $0 # jnz Label # ret # quitter_region mov verrou, $0 ret
Copier verrou dans reg. verrou = 1 verrou = 0 ? si different on boucle (verrou) ret. entrer section critique
11
sa section critique est suspendu si un autre processus B est dj dans sa section critique. Le processus H sera rveill par le processus B, lorsquil quitte la section critique. Problme : Si les tests et les mises jour des conditions dentre en section critique ne sont pas excuts en exclusion mutuelle , des problmes peuvent survenir. Si le signal mis par WAKEUP() arrive avant que le destinataire ne soit endormi, le processus peut dormir pour toujours. Exemples
// Processus P1 { if(cond ==0 ) SLEEP() ; cond = 0 ; section_critique ( ) ; cond = 1 ; WAKEUP(P2) ; ... } // Processus P2 { if (cond ==0 ) SLEEP() ; cond = 0 ; section_critique ( ) ; cond = 1 ; WAKEUP(P1) ; ... }
Cas 1 : Supposons que : P1 est excut et il est suspendu juste aprs avoir lu la valeur de cond qui est gale 1. P2 est lu et excut. Il entre dans sa section critique et est suspendu avant de la quitter. P1 est ensuite excut. Il entre dans sa section critique. Les deux processus sont dans leurs sections critiques. Cas 2 : Supposons que : P1 est excut en premier. Il entre dans sa section critique. Il est suspendu avant de la quitter ; P2 est lu. Il lit cond qui est gale 0 et est suspendu avant de la tester.
12
Dans le systme Unix, les appels systme qui assurent peu prs, les mmes fonctions que les primitives SLEEP et WAKEUP sont : pause( ) : suspend le processus appelant jusqu rception dun signal. kill(pid, SIGCONT) : envoie le signal SIGCONT au processus pid. Lmission dun signal est possible si lune des deux conditions est vrie : 1. Lmetteur et le destinataire du signal appartiennent au mme propritaire 2. Lmetteur du signal est le super-utilisateur.
13
Processus producteur
Processus consommateur
Flux de donnes
Mcanisme de communication
} 20
30
void consomateur ( ) { while (TRUE) { i f ( ! compteur ) SLEEP ( ) ; retirer_objet ( ) ; compteur ; i f ( compteur = = N 1 ) WAKEUP( producteur ) ; consommer_objet ( ) ; } }
14
6.7 Smaphores
Pour contrler les accs un objet partag, E. W. Dijkstra suggra en 1965 lemploi dun nouveau type de variables appeles les smaphores. Un smaphore est un compteur entier qui dsigne le nombre dautorisations daccs une section critique. Il a donc un nom et une valeur initiale, par exemple semaphore S = 10. Les smaphores sont manipuls au moyen des oprations P ou wait et V ou signal. Lopration P(S) dcrmente la valeur du smaphore S si cette dernire est suprieure 0. Sinon le processus appelant est mis en attente. Le test du smaphore, le changement de sa valeur et la mise en attente ventuelle sont effectus en une seule opration atomique indivisible. Lopration V(S) incrmente la valeur du smaphore S, si aucun processus nest bloqu par lopration P(S). Sinon, lun dentre eux sera choisi et redeviendra prt. V est aussi une opration indivisible. Les smaphores permettent de raliser des exclusions mutuelles, comme illustr la gure 6.4. semaphore mutex=1 ; processus P1 : P(mutex) section critique de P1 ; V(mutex) ; processus P2 ; P(mutex) section critique de P2 ; V(mutex) ;
F IG . 6.4 Exclusion mutuelle avec des smaphores. La gure 6.5 montre lexclusion mutuelle dune petite section critique a++. int a ; semaphore mutex=1 ; processus P1 : processus P2 ; P(mutex) P(mutex) a++ ; a++ ; V(mutex) ; V(mutex) ; F IG . 6.5 Exclusion mutuelle dune section critique compose dune variable a. Sur la gure 6.6 on montre la valeur que les smaphores prennent lors des appels des P (wait) et V (signal), pour trois processus , et
15
.
Valeur du smaphore (s)
1 0 -1 -2 wait(s) wait(s) wait(s)
P0
P1
P2
dbloquer
-1
signal(s)
dbloquer
signal(s)
signal(s)
16
int sem_trywait(sem_t *sem) dcrmente la valeur du smaphore sem si sa valeur est suprieure 0 ; sinon elle retourne une erreur. Cest une opration atomique. int sem_getvalue (sem_t* nom, int * sval) ; rcuprer la valeur dun smaphore : il retourne toujours 0. int sem_destroy (sem_t* nom) ; dtruire un smaphore. Retourne -1 sil y a encore des waits. Les smaphores Posix servent donc synchroniser les communications entre threads. Les smaphores dUnix System V sont un autre type de mcanisme qui permet la synchronisation entre processus. Sa gestion tant un peu plus complexe, on verra ce mcanisme dans le Chapitre ??, Communications IPC System V. Exemple 2. Le programme sem-posix.c sem-posix.c montre lutilisation de smaphores Posix.
10
);
20
i n t main ( i n t a r g c , char argv [ ] ) { int n , i , error ; p t h r e a d _ t t h r e a d i d [MAXTHREADS] ; i f ( ( a r g c ! = 2 ) | | ( ( n = a t o i ( argv [ 1 ] ) ) < = 0 ) ) { p r i n t f ( " Usage : % s nombre de t h r e a d s \n " , argv [ 0 ] ) ; exit (1 ); } // i n i t i a l i s e r l e smaphore s e m _ i n i t(&my_sem , 0 , SEMINITVALUE ) ; // c r a t i o n des t h r e a d s for ( i = 0 ; i < n ; ++ i ) p t h r e a d _ c r e a t e (& t h r e a d i d [ i ] , NULL, c r i t i c a l _ s e c t i o n , ( void // a t t e n d r e l a f i n des t h r e a d s for ( i = 0 ; i < n ; ++ i )
) i );
17
30
40
void c r i t i c a l _ s e c t i o n ( void arg ) { i n t myindex , i , n ; // e x t r a i r e l index des t h r e a d s myindex = ( i n t ) arg ; sem_wait(&my_sem ) ; // s e c t i o n c r i t i q u e f o r ( i = 1 ; i < 5 ; i ++) { p r i n t f ( "%d : % d\n " , myindex , i ) ; i f ( ( n= s l e e p ( 1 ) ) ! = 0 ) // dormir 1 seconde p r i n t f ( " i n t e r r u p t e d , no o f s e c s l e f t %d\n " , n ) ; } // q u i t e r s e c t i o n c r i t i q u e sem_post (&my_sem ) ; p t h r e a d _ e x i t (NULL ) ; }
Exemple 3. Producteur/consommateur avec smaphores. La solution du problme au moyen des smaphores Posix utilise trois smaphores dans le programme prod-cons.cpp. Le premier smaphore, nomm plein, compte le nombre demplacements occups. Il est initialis 0. Le second, nomm vide, compte le nombre demplacements libres. Il est initialis N (la taille du tampon). Le dernier, nomm mutex, assure lexclusion mutuelle pour laccs au tampon (lorsquun processus utilise le tampon, les autres ne peuvent pas y accder).
18
< semaphore . h> < u n i s t d . h > // s l e e p < pthread . h> < s t d i o . h>
10
# define t a i l l e 3 sem_t p l e i n , vide , mutex ; i n t tampon [ t a i l l e ] ; p t h r e a d _ t cons , prod ; void consommateur ( void ) ; void producteur ( void ) ; i n t main ( ) { // i n i t i l i s e r l e s smaphores s e m _ i n i t(& p l e i n , 0 , 0 ) ; s e m _ i n i t(& vide , 0 , t a i l l e ) ; s e m _ i n i t(&mutex , 0 , 1 ) ; // c r e r l e s t h r e a d s p t h r e a d _ c r e a t e (& cons ,NULL, consommateur ,NULL) ; p t h r e a d _ c r e a t e (&prod ,NULL, producer ,NULL) ; // a t t e n d r e l a f i n des t h r e a d s p t h r e a d _ j o i n ( prod ,NULL) ; p t h r e a d _ j o i n ( cons ,NULL) ; p r i n t f ( " f i n des t h r e a d \ n " ) ; return 0 ; } void consommateur ( void ) { i n t i c = 0 , nbcons = 0 , o b j e t ; do { sem_wait(& p l e i n ) ; sem_wait(&mutex ) ; // consommer o b j e t = tampon [ i c ] ; sem_post (&mutex ) ; sem_post (& vide ) ; p r i n t f ( " i c i cons . : tampon[%d]= %d\n " , i c , o b j e t ) ; i c =( i c +1)% t a i l l e ; nbcons ++; sleep ( 2 ) ; } while ( nbcons < = 5 ) ; r e t u r n ( NULL) ; } void producteur ( void
20
30
40
19
60
20
chaque philosophe de se livrer ses activits (penser et manger) sans jamais tre bloqu.
F IG . 6.7 Problme des philosophes. Il est important de signaler que si tous les philosophes prennent en mme temps chacun une fourchette, aucun dentre eux ne pourra prendre lautre fourchette (situation dinterblocage). Pour viter cette situation, un philosophe ne prend jamais une seule fourchette. Les fourchettes sont les objets partags. Laccs et lutilisation dune fourchette doit se faire en exclusion mutuelle. On utilisera le smaphore mutex pour raliser lexclusion mutuelle.
< s t d i o . h> < unistd . h> // pour s l e e p < pthread . h > // pour l e s t h r e a d s < semaphore . h > // pour l e s smaphores
10
# define N 5 // nombre de p h i l o s o p h e s # define G ( ( N+ i 1)%N) // philosophe de gauche de i # define D (i) // philosophe de d r o i t e de i # define l i b r e 1 # define occupe 0 i n t fourch [N] = { l i b r e , l i b r e , l i b r e , l i b r e , l i b r e } ; sem_t mutex ; void philosophe ( void ) ;
21
20
30
40
50
60
22
Pour viter le problme de famine il faut garantir que si un processus demande dentrer en section critique, il obtient satisfaction au bout dun temps ni. Dans le cas des philosophes, le problme de famine peut tre vit, en ajoutant N smaphores (un smaphore pour chaque philosophe). Les smaphores sont initialiss 0. Lorsquun philosophe i ne parvient pas prendre les fourchettes, il se met en attente ( P(S[i]) ). Lorsquun philosophe termine de manger, il vrie si ses voisins sont en attente. Si cest le cas, il rveille les voisins qui peuvent manger en appelant lopration V. On distingue trois tats pour les philosophes : penser, manger et faim.
Exemple 5. Une meilleure solution au problme des philosophes. Listing 6.7 philosophe2.c
< s t d i o . h> < u n i s t d . h> < pthread . h> < semaphore . h>
10
# define N 5 # define G ( ( N+ i 1)%N) // philosophe de gauche de i # define D ( i ) // philosophe de d r o i t e de i enum e t a t { penser , faim , manger } ; e t a t E t a t [N] = { penser , penser , penser , penser , penser } ; sem_t S [N ] ; sem_t mutex ; void philosophe ( void ) ; void T e s t ( i n t i ) ; i n t main ( ) { i n t i , NumPhi [N ] = { 0 , 1 , 2 , 3 , 4 } ; p t h r e a d _ t ph [N] ; s e m _ i n i t (&mutex , 0 , 1 ) ; f o r ( i = 0 ; i <N ; i ++) s e m _ i n i t(&S [ i ] , 0 , 0 ) ; // c r a t i o n des N p h i l o s o p h e s f o r ( i = 0 ; i <N ; i ++) p t h r e a d _ c r e a t e (&ph [ i ] , NULL, philosophe , & (NumPhi [ i ] ) ) ; // a t t e n d r e l a f i n des t h r e a d s f o r ( i = 0 ; ( i <N && p t h r e a d _ j o i n ( ph [ i ] , NULL) = = 0 ) ; i + + ) ; p r i n t f ( " f i n des t h r e a d s \ n " ) ; return 0 ; }
20
30
23
40
50
60
Excution de philosophe2.c
leibnitz> g++ -o philosophe2 philosophe2.c -lpthread leibnitz> philosophe2 philosophe[0] mange philosophe[2] mange philosophe[4] mange philosophe[0] a fini de manger
24
philosophe[2] a fini philosophe[1] mange philosophe[4] a fini philosophe[3] mange philosophe[0] mange philosophe[1] a fini philosophe[2] mange philosophe[3] a fini philosophe[4] mange philosophe[0] a fini philosophe[2] a fini philosophe[1] mange philosophe[3] mange philosophe[4] a fini philosophe[1] a fini philosophe[3] a fini fin des threads leibnitz>
25
Lecteur
Lecteur
crivain
Lecteur
crivain
Ressource
F IG . 6.8 Problme des lecteurs et crivains. semaphore Redact // crivain semaphore Mutex // accs la base lecteur : repeat P(Mutex) if (NbL == 0) then P(Redact) // Premier lecteur empche criture end if NbL NbL + 1 V(Mutex) // lecture de la base // n de laccs la base P(Mutex) NbL NbL - 1 if (NbL == 0) then V(Redact) // Dernier lecteur habilite lcriture end if V(Mutex) until TRUE redacteur : repeat P(Redact) // modier les donnes de la base V(Redact) until TRUE
26
Exemple 6. Le programme red-lec.c fait limplantation de lalgorithme des rdacteurs et lecteurs. Il xe le nombre de lecteurs et rdacteurs N=3, ainsi que le nombre daccs (an dviter une boucle innie) ACCES=4. On a simul des oprations de lecture de la base, des modications des donnes et dcriture dans la base avec des sleep() dune seconde.
10
# define N 3 # d e f i n e ACCES 4 i n t NbL = 0 ; sem_t Redact ; sem_t mutex ; void l e c t e u r ( void ) ; void r e d a c t e u r ( void ) ; i n t main ( ) { int i ; i n t numred [N ] = { 0 , 1 , 2 } ; i n t numlec [N ] = { 0 , 1 , 2 } ; p t h r e a d _ t red [N] ; p t h r e a d _ t l e c [N] ; s e m _ i n i t(&mutex , 0 , 1 ) ; s e m _ i n i t(& Redact , 0 , 1 ) ; f o r ( i = 0 ; i <N ; i ++) { // c r a t i o n des r e d a c t e u r s p t h r e a d _ c r e a t e (& red [ i ] , NULL, l e c t e u r , & ( numred[ i ] ) ) ; // c r a t i o n des l e c t e u r s p t h r e a d _ c r e a t e (& l e c [ i ] , NULL, r e d a c t e u r , & ( numlec [ i ] ) ) ; } // a t t e n d r e l a f i n des t h r e a d s f o r ( i = 0 ; i <N ; i ++) { p t h r e a d _ j o i n ( red [ i ] , NULL) ; p t h r e a d _ j o i n ( l e c [ i ] , NULL) ; } p r i n t f ( " f i n des t h r e a d s \n " ) ; return 0 ; }
20
30
27
50
60
70
28
Redacteur 0 Lecteur 0 Lecteur 1 Lecteur 2 Redacteur 1 Redacteur 2 Redacteur 0 Lecteur 0 Lecteur 1 Redacteur 1 Lecteur 2 Redacteur 2 Redacteur 0 Lecteur 0 Lecteur 1 Redacteur 1 Lecteur 2 Redacteur 2 fin des threads leibnitz>
6.9.1 Producteur/consommateur
Pour le problme du producteur et du consommateur, nous pouvons utiliser deux compteurs dvnements in et out. Le premier compte le nombre dinsertions dans le tampon alors que le second compte le nombre de retraits du tampon depuis le dbut.
29
in : est incrmente de 1 par le producteur aprs chaque insertion dans le tampon. out : est incrmente de 1 par le consommateur aprs chaque retrait du tampon. Le producteur doit sarrter de produire lorsque le tampon est plein. La condition satisfaire pour pouvoir produire est : N > in - out. Posons sequence = in + 1. La condition devient alors : out sequence - N De mme, le consommateur peut consommer uniquement si le tampon nest pas vide. La condition satisfaire pour pouvoir consommer est : in - out > 0. Posons sequence = out + 1. La condition devient alors : in sequence.
// producteur consommateur #define N 100 // taille du tampon int in = 0, out = 0 ; void producteur (void) { int objet , sequence =0, pos =0 ; while (1) { objet = objet+1 ; sequence = sequence + 1 ; // attendre que le tampon devienne non plein Await(out, sequence-N) ; tampon[pos]= objet ; pos = (pos+1)%N ; // incrmenter le nombre de dpts Advance(&int) ; } } void consommateur (void) { int objet , sequence =0, pos =0 ; while (1) { sequence = sequence + 1 ; // attendre que le tampon devienne non vide Await(in,sequence) ; objet = tampon[pos] ;
30
6.10 Moniteurs
Lide des moniteurs est de regrouper dans un module spcial, appel moniteur, toutes les sections critiques dun mme problme. Un moniteur est un ensemble de procdures, de variables et de structures de donnes. Les processus peuvent appeler les procdures du moniteur mais ils ne peuvent pas accder la structure interne du moniteur partir des procdures externes. Pour assurer lexclusion mutuelle, il suft quil y ait tout moment au plus un processus actif dans le moniteur. Cest le compilateur qui se charge de cette tche. Pour ce faire, il rajoute, au dbut de chaque procdure du moniteur un code qui ralise ce qui suit : sil y a processus actif dans le moniteur, alors le processus appelant est suspendu jusqu ce que le processus actif dans le moniteur se bloque en excutant un wait sur une variable conditionnelle (wait(c)) ou quitte le moniteur. Le processus bloqu est rveill par un autre processus en lui envoyant un signal sur la variable conditionnelle ( signal(c) ). Cette solution est plus simple que les smaphores et les compteurs dvnements, puisque le programmeur na pas se proccuper de contrler les accs aux sections critiques. Mais, malheureusement, lexception de JAVA, la majorit des compilateurs utiliss actuellement ne supportent pas les moniteurs.
6.10.1 Producteur/consommateur
Les sections critiques du problme du producteur et du consommateur sont les oprations de dpt et de retrait dans le tampon partag. Le dpt est possible si le tampon nest pas plein. Le retrait est possible si le tampon nest pas vide.
Moniteur ProducteurConsommateur { //variable conditionnelle pour non plein et non vide bool nplein, nvide ; int compteur =0, ic=0, ip=0 ;
31
// section critique pour le dpt void mettre (int objet) { if (compteur==N) wait(nplein) ; //attendre jusqu ce que le tampon devienne non plein // dposer un objet dans le tampon tampon[ip] = objet ; ip = (ip+1)%N ; compteur++ ; // si le tampon tait vide avant le dpt, // envoyer un signal pour rveiller le consommateur. if (compteur==1) signal(nvide) ; } // section critique pour le retrait void retirer (int* objet) { if (compteur ==0) wait(nvide) ; objet = tampon[ic] ; compteur -- ; if(compteur==N-1) signal(nplein) ; } }
32
10
20
30
i f ( fork ( ) = = 0 ) { for ( ; ; ) { read ( f i l d e s [ 0 ] , & c , 1 ) ; // e n t r e s e c t i o n c r i t i q u e // < Section c r i t i q u e > w r i t e ( f i l d e s [ 1 ] , & c , 1 ) ; // q u i t e r s e c t i o n c r i t i q u e } } e l s e // p r o c e s s u s pre { for ( ; ; ) { read ( f i l d e s [ 0 ] , & c , 1 ) ; // e n t r e s e c t i o n c r i t i q u e // < section c r i t i q u e > w r i t e ( f i l d e s [ 1 ] , & c , 1 ) ; // q u i t t e r s e c t i o n c r i t i q u e } } return 0 ;
33
34 Dverrouillage
!
argv )
10
N = a t o i ( argv [ 1 ] ) ; // Nombre sum = 0 ; // somme f o r ( i = 1 ; i <=N; + + i ) { sum + = i ; } p r i n t f ( "Somme = % 1 0 . 0 f \n " , sum ) ; return ( 0 ) ;
Le programme sum-thread.c somme les mmes N nombres, mais en forme parallle avec deux threads qui utilisent mutex.
10
void p r o c e s s u s ( void arg ) { double localsum ; int i ; i n t i p r o c = ( ( ( char ) arg ) 0 ) ; localsum = 0 ; // somme f o r ( i = i p r o c ; i <=N ; i +=2) { localsum + = i ;
35
20 }
i n t main ( i n t a r g c , char ! argv ) { pthread_t thread0 , thread1 ; void r e t v a l ; 30 N = a t o i ( argv [ 1 ] ) ; // Nombre // I n i t i a l i s e r l e l o c k de l a somme p t h r e a d _ m u t e x _ i n i t (&somme_lock , NULL) ; // 2 threads i f ( p t h r e a d _ c r e a t e (& t h r e a d 0 , NULL , p r o c e s s u s , " 0 " ) | | p t h r e a d _ c r e a t e (& t h r e a d 1 , NULL , p r o c e s s u s , " 1 " ) ) { p r i n t f ( "%s : e r r e u r t h r e a d\n " , argv [ 0 ] ) ; exit (1); } // J o i n l e s deux t h r e a d s i f ( pthread_join ( thread0 , & r e t v a l ) || pthread_join ( thread1 , & r e t v a l ) ) { p r i n t f ( "%s : e r r e u r j o i n \n " , argv [ 0 ] ) ; exit (1); } p r i n t f ( "Somme ( p a r t a g e ) = % 1 0 . 0 f \n " , somme ) ; return 0 ;
40
Exemple 8. Soit un tableau M de N lments rempli par un thread lent et lu par un autre plus rapide. Le thread de lecture doit attendre la n du remplissage du tableau avant dafcher son contenu. Les mutex peuvent protger le tableau pendant le temps de son remplissage.
36
i n t M[N] ;
10
arg )
pthread_mutex_lock(&mutex ) ; f o r ( i = 0 ; i < N ; i ++) p r i n t f ( " L c t u r e : M[%d ] = % d\n " , i , M[ i ] ) ; pthread_mutex_unlock (&mutex ) ; pthread_exit ( 0 ) ; } 20 void e c r i t u r e ( void { int i ;
arg )
30 }
pthread_mutex_lock(&mutex ) ; f o r ( i = 0 ; i < N ; i ++) { M[ i ] = i ; p r i n t f ( " c r i t u r e : M[%d ] = % d\n " , i , M[ i ] ) ; s l e e p ( 2 ) ; // R a l e n t i r l e t h r e a d d e c r i t u r e } pthread_mutex_unlock (&mutex ) ; pthread_exit ( 0 ) ; i n t main ( void ) { p t h r e a d _ t th1 , th2 ; void r e t ;
40
p t h r e a d _ m u t e x _ i n i t (&mutex , NULL) ; p t h r e a d _ c r e a t e (& th1 , NULL , e c r i t u r e , NULL) ; p t h r e a d _ c r e a t e (& th2 , NULL , l e c t u r e , NULL ) ; p t h r e a d _ j o i n ( th1 , & r e t ) ; p t h r e a d _ j o i n ( th2 , & r e t ) ; return 0 ;
37
Sans lutilisation des mutex, probablement on obtiendrait des sorties rrones comme la suivante :
criture: M[0] = 0 Lcture: M[0] = 0 Lcture: M[1] = 0 Lcture: M[2] = 0 Lcture: M[3] = 0 Lcture: M[4] = 0 criture: M[1] = 1 criture: M[2] = 2 criture: M[3] = 3 criture: M[4] = 4
o sp est un pointeur sur le smaphore initialiser ; count est la valeur initiale du smaphore ; type indique si le smaphore est utilis pour synchroniser des processus lgers (threads) ou des processus. Le type peut tre USYNC_PROCESS ou bien USYNC_THREAD. Par dfaut, le type sera USYNC_THREAD. int sema_wait (sema_t *sp) est quivalente lopration P.
38
10
20
30
39
50
10
sema_t p l e i n , vide , mutex ; i n t tampon [ t a i l l e ] ; void consommateur ( void ) ; void producer ( void ) ; i n t main ( ) {
40
20
30
40
void consommateur ( void r e t r a i t ) { i n t i c = 0 , nbcons = 0 , o b j e t ; do { sema_wait (& p l e i n ) ; sema_wait (&mutex ) ; // consommer o b j e t = tampon [ i c ] ; sema_post (&mutex ) ; sema_post (& vide ) ; p r i n t f ( "\n i c i cons . : tampon[%d]= %d\n " , i c , objet ) ; i c =( i c +1)% t a i l l e ; nbcons ++; sleep ( 2 ) ; } while ( nbcons < = 5 ) ; r e t u r n ( NULL) ; } void producer ( void ) { i n t i p = 0 , nbprod = 0 , o b j e t = 0 ; do { sema_wait (& vide ) ; sema_wait (&mutex ) ; // produire tampon [ i p ]= o b j e t ; sema_post (&mutex ) ; sema_post (& p l e i n ) ; p r i n t f ( "\n i c i prod . : tampon[%d]= %d\n " , i p , objet ) ; o b j e t ++; nbprod ++; i p =( i p +1)% t a i l l e ;
50
60
41
10
# define N 5 // nombre de p h i l o s o p h e s # d e f i n e G (N+ i 1)%N // philosophe gauche de i # d e f i n e D ( i +1)%N // philosophe d r o i t e de i # define l i b r e 1 # d e f i n e occupe 0 i n t fourch [N] = { l i b r e , l i b r e , l i b r e , l i b r e , l i b r e } ; sema_t mutex ; void philosophe ( void ) ; i n t main ( ) { i n t NumPhi [N ] = { 0 , 1 , 2 , 3 , 4 } ; s e m a _ i n i t (&mutex , 1 , NULL , NULL) ; // c r a t i o n des N p h i l o s o p h e s
42
20
30
40
50
// La f o n c t i o n de chaque philosophe void philosophe ( void num) { i n t i = ( i n t ) num ; i n t nb = 2 ; while ( nb ) { // penser sleep ( 1 ) ; // e s s a y e r de prendre l e s f o u r c h e t t e s pour manger sema_wait (&mutex ) ; i f ( fourch [G] & & fourch [ i ] ) { fourch [G ] = 0 ; fourch [ i ] = 0 ; sema_post (&mutex ) ; nb ; // manger p r i n t f ( " philosophe [%d ] mange\n " , i ) ; s l e e p ( 1 ) ; // manger p r i n t f ( " philosophe [%d ] a f i n i de manger\n " , i ) ; // l i b r e r l e s f o u r c h e t t e s sema_wait (&mutex ) ; fourch [G ] = 1 ; fourch [ i ] = 1 ; sema_post (&mutex ) ; } e l s e sema_post (&mutex ) ; } }
43
Exemple 12. Solution amliore des Philosophes (Solaris) version 2 Listing 6.16 v-philosophe2.c
# i n c l u d e < s t d i o . h> # i n c l u d e < u n i s t d . h> # i n c l u d e < t h r e a d . h> # i n c l u d e < synch . h> # define N 5 # d e f i n e G (N+ i 1)%N # d e f i n e D ( i +1)%N
// philosophe // philosophe
gauche de i d r o i t e de i
10
enum e t a t { penser , faim , manger } ; i n t fourch [N] = { l i b r e , l i b r e , l i b r e , l i b r e , l i b r e } ; e t a t E t a t [N] = { penser , penser , penser , penser , penser } ; sema_t S [N ] ; sema_t mutex ; void philosophe ( void ) ; i n t main ( ) { i n t i , NumPhi [N ] = { 0 , 1 , 2 , 3 , 4 } ; s e m a _ i n i t (&mutex , 1 , NULL , NULL ) ; f o r ( i = 0 ; i <N ; i ++) sema_int (&S [ i ] , 0 , NULL,NULL ) ; // c r a t i o n des N p h i l o s o p h e s f o r ( i = 0 ; i <N ; i ++) t h r _ c r e a t e (NULL, 0 , philosophe , & (NumPhi [ i ] ) , 0 , NULL) ; // a t t e n d r e l a f i n des t h r e a d s
20
44
} 30
40
50
void philosophe ( void num) { i n t i = ( i n t ) num ; i n t nb = 2 ; while ( nb ) { nb ; // penser sleep ( 1 ) ; // t e n t e r de manger sema_wait (&mutex ) ; E t a t [ i ]= faim ; test ( i ) ; sema_post (&mutex ) ; sema_wait (&S [ i ] ) ; // manger p r i n t f ( " philosophe [%d ] mange\n " , i ) ; s l e e p ( 1 ) ; // manger p r i n t f ( " philosophe [%d ] a f i n i de manger\n " , i ) ; // l i b r e r l e s f o u r c h e t t e s sema_wait (&mutex ) ; E t a t [ i ] = penser ; // v r i f i e r s i s e s v o i s i n s peuvent manger T e s t (G ) ; T e s t (D ) ; sema_post (&mutex ) ; } } // procdure qui v r i f i e s i l e philosophe i peut manger void T e s t ( i n t i ) { i f ( ( E t a t [ i ] = = faim ) & & ( E t a t [G ] ! = manger ) && ( E t a t [D] ! = manger ) ) { E t a t [ i ] = manger ; sema_post (&S [ i ] ) ; } }
60
45
de manger de manger
46
6.15 Exercises
1. Pourquoi le partage de donnes pose des problmes dans un systme multiprogramm en temps partag ? 2. Le systme Unix permet-il de contrler les accs aux donnes partages ? 3. Quest-ce quune section critique ? 4. Expliquez la raison pour laquelle il est ncessaire de synchroniser les processus. Est-ce que tous les types de SE utilisent la synchronisation de processus ? 5. Identiez et expliquez un problme typique qui ncessiterait la synchronisation de processus. 6. Identiez le point (i.e. les noncs) dun processus qui reprsente sa section critique. 7. Identiez le problme principal qui peut survenir lorsque laccs la section critique dun processus est contrl par une variable boolenne de type verrou. Donnez une solution ce problme. 8. Quel est le problme principal que rsout un smaphore en ce qui concerne laccs une section critique ? 9. Quand doit-on utiliser des smaphores binaire et gnral ? Peut-on utiliser ces deux types de smaphores dans un mme processus et pour un mme problme de synchronisation ? Expliquez. 10. Pourquoi un tube Unix utilise gnralement deux canaux de communication entre deux processus, soit un canal dentre et un canal de sortie ? Existe-il des alternatives ce mode de communication utilisant les tubes Unix ? 11. Considrez lapplication suivante qui implante un systme interactif qui lit dans une boucle des caractres entrs via le clavier et qui les afche lcran. Le traitement se termine lorsque lusager presse la touche <Ctrl-d> pour insrer le caractre de n de chier (EOF) dans le tampon.
// Processus parent char *tampon; // tampon partag par les processus int i=0, j=0; // positions de lecture et dcriture dans le tampon // Processus lecture ...
6.15. EXERCISES
while (read(stdin, tampon, 1) != NULL) ... // Processus criture ... while (write(stdout, tampon, 1) != NULL) ...
47
(a) Synchronisez la communication entre les processus lecture et criture laide de tubes et dappels systme fork. (b) Synchronisez la communication entre les threads lecture et criture laide de smaphores. (c) Considrez la solution suivante au problme daccs des ressources partages en mode de lecture et dcriture. Les processus en lecture et en criture sont en comptition pour laccs une ressource partage. Un seul processus la fois peut tre dans sa section critique, ceci est valable autant pour les processus en lecture et en criture. Le systme ne fait pas de diffrence entre les processus en lecture et en criture, et chaque processus peut postuler une requte daccs la ressource qui lui sera accorde ds quun processus a ni de lire ou dcrire sur la ressource. Cependant, le systme dnit des processus prioritaires pour lcriture de la ressource, et les requtes de ces processus doivent tre honores avant celles des processus normaux en lecture et en criture ds que la section critique se libre. Montrez la synchronisation des processus en lecture et en criture, en utilisant des smaphores. 12. Considrez le buffer circulaire montr sur la gure 6.9, qui rpresente une variation du problme du producteur/consommateur. Rsoudre le problme avec des smaphores. 13. Le code suivant contient une solution propose au problme daccs une section critique. Expliquez pourquoi cette solution serait correcte ou incorrecte, selon quelle respecte les 4 principes fondamentaux rgissant laccs une section critique.
/* Variables partages par les processus P0 et P1. */ int flag[2]; // Tableau de boolens stockant 0 ou 1 // Variable indiquant le processus actif, // i.e. 0 pour P0 ou 1 pour P1
48
Producteur
Consomateur
int turn; // Code du processus P0 do { flag[0] = 1; // Mettre le flag de P0 1 while (turn != 0) // Si P0 est inactif alors, ... { while (flag[1]) // Attendre linactivit de P1 { /* skip */ } turn = 0; // Indique que P0 est maintenant actif } ... <Section critique pour le processus P0> ... // Rinitialiser le flag de P0 afin que P1 puisse // sactiver dans sa section critique flag[0] = 0; ... <Autres noncs pour P0> ... } while (1); /* Code du processus P1 */ do { flag[1] = 1; /* Mettre le flag de P1 1 */ while (turn != 1) /* Si P1 est inactif alors, ... */ { while (flag[0]) /* Attendre linactivit de P0 */ { /* skip */ }
6.15. EXERCISES
49
turn = 1; /* Indiquez que P1 est maintenant actif */ } ... <Section critique pour le processus P1> ... // Rinitialiser le flag de P1 afin que P0 puisse // sactiver dans sa section critique flag[1] = 0; ... <Autres noncs pour P1> ... } while (1);
14. Dijkstra a propos les 3 solutions suivantes comme tant des solutions logicielles au problme daccs une section critique. Par contre, aux vues de Dijkstra ces solutions ntaient pas tout fait correctes. Alors, expliquez pourquoi chacune des solutions est effectivement incorrecte. a ) int turn;
proc(int i) { while (TRUE) { compute; while (turn != i); <section critique> turn = (i+1) mod 2; } } turn = 1; fork(proc, 1, 0); fork(proc, 1, 1);
b ) boolean flag[2];
proc(int i) { while (TRUE) { compute; while (flag[(i+1) mod 2]); flag[i] = TRUE; <section critique> flag[i] = FALSE; } } flag[0] = flag[1] = FALSE; fork(proc, 1, 0); fork(proc, 1, 1);
c ) boolean flag[2];
50
15. Considrez une route deux voies (une voie par direction) et oriente selon un axe nord-sud. Cette route traverse un tunnel qui ne possde quune seule voie, et par consquent son accs doit tre partag en temps par les voitures provenant de chacune des deux directions (nord ou sud). An de prvenir les collisions, des feux de circulation ont t installs chacune des extrmits du tunnel. Un feu vert indique quil ny a aucune voiture dans le tunnel provenant de la direction oppose, et un feu rouge indique quil y a une voiture contresens dans le tunnel. Le trac dans le tunnel est contrl par un ordinateur central qui est prvenu de larrive de vhicules par des senseurs poss dans chaque direction. Lorsquun vhicule approche le tunnel en direction nord ou sud, le senseur externe excute la fonction arrive(direction) pour prvenir lordinateur central. Similairement, lorsquune voiture sort du tunnel le senseur interne excute la fonction dpart (direction). Concevez une esquisse de code pour synchroniser laccs au tunnel an que les feux de circulation fonctionnent correctement dans les deux directions. 16. On a 3 processus , chaque processus #%$'&
de
6.15. EXERCISES
je suis le processus 1 je suis le processus 2 je suis le processus 3 ...
51
Synchroniser les 3 processus laide des smaphores binaires. 17. Le programme suivant permet de calculer une estimation de la valeur de 7 avec un ot unique dexcution. Paralllisez ce programme laide dexclusion mutuelle de threads et en respectant lesprit de la boucle for (i=0 ; i<intervals ; ++i){...}.
#include <stdlib.h> #include <stdio.h> int main(int argc, char **argv) { double width, sum; int intervals, i; // nombre dintervalles intervals = atoi(argv[1]); width = 1.0 / intervals; // calcul de pi sum = 0; for (i=0; i<intervals; ++i) { double x = (i + 0.5) * width; sum += 4.0 / (1.0 + x * x); } sum *= width; printf("Estimation de pi = %10.9f\n", sum); return(0); }