TD5 Threads PDF
TD5 Threads PDF
TD5 Threads PDF
Objectifs : apprendre à créer, travailler avec et arrêter des threads (ou proces-
sus légers). Savoir reconnaître les données partagées entre différents threads.
Être capable d’orchestrer la synchronisation de threads au moyen des primi-
tives de terminaison ou de sémaphores d’exclusion mutuelle.
1 Notes de cours
1.1 Rappels sur le contexte d’un processus
Le contexte d’un processus est l’ensemble des structures de données nécessaires au système
pour assurer le contrôle de ce processus. Elles se répartissent en deux catégories :
1. Structures dédiées au contrôle des ressources :
– Masque de création des fichiers (umask) qui spécifie les bits de permission à exclure
lors de la création de fichiers ou répertoires.
– Propriétaires et groupes propriétaires réels (correspondant à celui qui a demandé la
création du processus) et effectifs (qui détermine les droits du processus par rapport
aux fichiers du système, souvent identité du propriétaire du fichier contenant le binaire
exécuté).
– Liste des descripteurs des fichiers ouverts.
– Répertoire de travail.
– Implantation en mémoire des données et du programme, qui comprend le segment
de code ou text segment (le code objet et exécutable du programme), le segment de
données ou data segment (les variables globales et static), et le tas ou heap segment
(la mémoire allouée dynamiquement par malloc()).
2. Structures dédiées à l’exécution du processus :
– Informations nécessaires à l’ordonnanceur (priorité, politique d’ordonnancement...).
– Valeur des registres, en particulier le compteur ordinal.
– Informations relatives aux signaux.
– Pile d’exécution ou stack segment (contenant la pile des appels de fonction, les argu-
ments et les variables locales).
1.2 Threads
La création d’un nouveau processus par la primitive fork() nécessite la copie complète du
contexte du processus père. Cela a l’avantage de la simplicité mais est d’une part particu-
lièrement coûteux en temps d’exécution pour le système et d’autre part pas toujours adapté
aux applications avec beaucoup de parallélisme. Les threads, qu’on appelle aussi processus
légers ou activités, sont des unités d’exécution des processus : ils travaillent directement avec
les structures de données dédiées au contrôle (voir Section 1.1, catégorie 1) du processus père,
qu’on appellera plus justement activité initiale. Leur temps de création est minimal pour le
Travaux Dirigés no 5 Threads 2/7
système (qui n’a plus besoin que de copier les structures de données dédiées à l’exécution, voir
Section 1.1, catégorie 2). Cependant leur manipulation est plus délicate pour les programmeurs
qui doivent être parfaitement conscients des problèmes liés au travail en mémoire partagée et à
l’aise avec les solutions telles que les sémaphores d’exclusion mutuelle.
En particulier, on notera que le code, les variables globales et la mémoire allouée dynami-
quement sont partagés entre les différentes activités d’un même processus. De même, bien
qu’elle ne soit connue directement que d’une activité, une variable locale dans la pile d’une
activité peut être lue ou modifiée par une autre activité si elle en connaît l’adresse.
1.2.1 Création
# i n c l u d e < p t h r e a d . h>
int pthread_create (
pthread_t ∗ p_tid , /∗ P o i n t e u r s u r i d e n t i t e du t h r e a d ∗ /
pthread_attr_t ∗ p_attr , /∗ NULL : a t t r i b u t s p a r d e f a u t ∗ /
void ∗ (∗ f o n c t i o n ) ( void ∗ ar g ) , / ∗ F onct i on e x e c u t e e par l e t h r e a d ∗ /
void ∗ ar g /∗ P a r a m e t r e de << f o n c t i o n >> ∗ /
);
La primitive pthread_create crée une nouvelle activié et renvoie son identité à l’adresse
p_tid (il s’agît d’un numéro entier non signé qui servira par la suite à la gestion du thread).
Son argument attr définit les attributs du thread (nous utiliserons toujours NULL, qui donne les
attributs par défaut), fonction est un pointeur sur la fonction qui sera exécutée par l’activité
(cette fonction retourne nécessairement un void * et prend nécessairement un unique argu-
ment de type void *). Enfin, le dernier argument arg correspond à l’argument transmis à la
fonction fonction. Cette primitive retourne 0 en cas de succès, un code d’erreur sinon.
1.2.2 Identité
Chaque processus a un numéro unique, le pid, qui est renvoyé par la primitive getpid().
Tout processus est lui-même décomposé en threads qui ont chacun leur identifiant unique pour
un même processus, le tid. On notera l’activité tid (par exemple 5) du processus pid (par
exemple 1234) sous la forme pid.tid (par exemple 1234.5). Deux primitives sont dédiées à
la manipulation des identités des activités :
# i n c l u d e < p t h r e a d . h>
p t h r e a d _ t p t h r e a d _ s e l f ( void ) ;
int pthread_equal ( pthread_t tid_1 , pthread_t tid_2 ) ;
1.2.3 Terminaison
Tous les threads d’un même processus prennent fin si l’activité initiale prend fin ou si une des
activités fait appel à la primitive exit() (ou _exit()). Une activité seule prend fin automati-
quement quand la fonction passée en argument de la primitive pthread_create retourne. Les
ressources allouées pour une activité ne sont jamais libérées automatiquement. Les primitives
de terminaison d’un thread (sans affecter les autres activités) et de libération de ses ressources
sont les suivantes :
# i n c l u d e < p t h r e a d . h>
void p t h r e a d _ e x i t ( void ∗ p _ s t a t u s ) ;
int pthread_detach ( pthread_t t id ) ;
1.2.4 Synchronisation
Les threads disposent dans les grandes lignes des mêmes outils de synchronisation que les pro-
cessus. Le premier d’entre eux est l’attente passive de la fin d’une autre activité qui rappelle
les primitives wait() ou waitpid() liées aux processus. La primitive permettant ce compor-
tement est la suivante :
# i n c l u d e < p t h r e a d . h>
i n t p t h r e a d _ j o i n ( p t h r e a d _ t t i d , v o i d ∗∗ s t a t u s ) ;
Cette primitive suspend l’exécution de l’activité appelante jusqu’à la fin de l’activité d’iden-
tifiant tid. Si l’activité d’identifiant tid est déjà terminée, cette primitive retourne immédia-
tement. En cas de succès, elle retourne 0 et place à l’adresse *status la valeur de retour de
l’activité attendue. En cas d’echec elle retourne un code d’erreur.
L’autre principal outil de synchronisation entre threads est le sémaphore d’exclusion mutuelle.
On manipule les sémaphores pour les threads à l’aide des quatres primitives fondamentales
suivantes qui retournent toutes 0 en cas de succès et un code d’erreur sinon :
# i n c l u d e < p t h r e a d . h>
i n t p t h r e a d _ m u t e x _ i n i t ( p t h r e a d _ m u t e x _ t ∗ p_mutex , NULL ) ;
i n t p t h r e a d _ m u t e x _ l o c k ( p t h r e a d _ m u t e x _ t ∗ p_mutex ) ;
i n t p t h r e a d _ m u t e x _ u n l o c k ( p t h r e a d _ m u t e x _ t ∗ p_mutex ) ;
i n t p t h r e a d _ m u t e x _ d e s t r o y ( p t h r e a d _ m u t e x _ t ∗ p_mutex ) ;
2 Exercices
2.1 Exercice 1 : partage des données et terminaison
On considère le code suivant où plusieurs threads sont crées, chacun ayant pour unique travail
d’afficher son identité :
# i n c l u d e < s t d i o . h>
# i n c l u d e < s t d l i b . h>
# i n c l u d e < p t h r e a d . h>
# d e f i n e NB_THREADS 3
int thread_execute = 0;
p t h r e a d _ t t i d [NB_THREADS ] ;
/ ∗ F o n c t i o n e x e c u t e e p a r l e s t h r e a d s . Le t y p e de r e t o u r e t l ’ a r g u m en t s o n t
∗ o b l i g a t o i r e m e n t de t y p e v o i d ∗ , ce q u i n e c e s s i t e s o u v e n t d e s c a s t s .
∗/
void ∗ f o n c t i o n ( void ∗ i ) {
i n t n = ∗ ( ( i n t ∗) i ) ;
p r i n t f ( " T h r ead numero %d , i d e n t i t e %d.%u \ n " , n , g e t p i d ( ) , p t h r e a d _ s e l f ( ) ) ;
thread_execute = 1;
}
i n t main ( ) {
int i ;
/ ∗ B o u c l e de c r e a t i o n d e s t h r e a d s . ∗ /
f o r ( i = 0 ; i <NB_THREADS ; i ++) {
i f ( p t h r e a d _ c r e a t e (& t i d [ i ] , NULL, f o n c t i o n , ( v o i d ∗)& i ) == −1) {
f p r i n t f ( s t d e r r , " E r r e u r c r e a t i o n t h r e a d numero %d . \ n " , i ) ;
exit (1);
}
}
p r i n t f ( " T h r ead i n i t i a l d ’ i d e n t i t e %d .%u \ n " , g e t p i d ( ) , p t h r e a d _ s e l f ( ) ) ;
i f ( thread_execute )
p r i n t f ( " Des t h r e a d s a n n e x e s o n t e t e e x e c u t e s . \ n " ) ;
else
p r i n t f ( " Aucun t h r e a d an n ex e n ’ a e t e e x e c u t e . \ n " ) ;
return 0;
}
Questions :
1. Combien au maximum, avec ce programme, y-a-t-il de threads s’exécutant en parallèle
(ou en concurrence s’il n’y a pas assez de ressources) ?
2. Listez toutes les variables et dire par quels threads elles sont directement utilisables.
3. Comment un thread pourrait lire ou modifier la variable n d’un autre thread ?
4. Expliquez le résultat d’exécution suivant où le numéro de chaque thread est le même.
Proposez une solution.
Thread numero 3, identite 13033.3084860304
Thread numero 3, identite 13033.3076467600
Thread numero 3, identite 13033.3068074896
Thread initial d’identite 13033.3084863168
Des threads annexes ont ete executes.
5. Expliquez le résultat d’exécution suivant où aucun thread n’a réalisé son affichage. Pro-
posez une solution.
Thread initial d’identite 13433.3084601024
Aucun thread annexe n’a ete execute.
(*)
(i,*)
(i)
for (i=0; i<NB_LIGNES; i++)
for (j=0; j<NB_COLONNES; j++) . =
y[i] += A[i][j] * x[j];
A x y
F IGURE 1 – Noyau de la multiplication matrice × vecteur
Note : pour les temporisations, utilisez seulement des primitives sleep(). L’utilisation du si-
gnal SIGALRM est possible (c’est d’ailleurs ce que fait sleep()) puisque les informations sur
les signaux sont liées aux threads et non au processus. Cependant il n’est pas possible d’uti-
liser la primitive signal() qui ne fonctionne pas comme elle le devrait avec les threads (elle
s’applique à tous les threads d’un même processus). À la place il faudrait utiliser la primitive
sigaction() qui n’est pas au programme (voir pages de man pour les curieux !).
# i n c l u d e < s t d i o . h>
# i n c l u d e < s t d l i b . h>
# i n c l u d e < p t h r e a d . h>
# define N_INF 0
# define N_SUP 200
# define X_MAX 50
# define T_INF 5
# define T_SUP 10
# define TIMEOUT 40
in t mystere ; / ∗ Nombre m y s t e r e ∗ /
p t h r e a d _ m u t e x _ t mutex ; / ∗ Semaphore de p r o t e c t i o n ∗ /
void ∗ fuyeur ( ) { / ∗ F o n c t i o n du t h r e a d f u y e u r ∗ /
int t , x ;
while ( 1 ) {
t = r a n d ( ) % ( T_SUP − T_INF + 1 ) + T_INF ; / ∗ Temps d ’ a t t e n t e ∗ /
x = r a n d ( ) % (X_MAX + 1 ) ; /∗ Modification ∗/
sleep ( t ) ;
p t h r e a d _ m u t e x _ l o c k (& mutex ) ; /∗ P r otection modification ∗/
i f ( rand ()%2) { / ∗ On a j o u t e ou on r e t i r e ∗ /
p r i n t f ( " Le nombre m y s t e r e a e t e au g m en te de %d ! \ n " , x ) ;
m y s t e r e = ( ( m y s t e r e + x ) > N_SUP ) ? N_SUP : m y s t e r e + x ;
}
else {
p r i n t f ( " Le nombre m y s t e r e a e t e d i m i n u e de %d ! \ n " , x ) ;
m y s t e r e = ( ( m y s t e r e − x ) < N_INF ) ? N_INF : m y s t e r e − x ;
}
p t h r e a d _ m u t e x _ u n l o c k (& mutex ) ; / ∗ F in de p r o t e c t i o n ∗ /
}
}
void ∗ timeout ( ) { / ∗ F o n c t i o n du t h r e a d t i m e o u t ∗ /
s l e e p ( TIMEOUT) ;
p r i n t f ( " Temps e c o u l e ! P er d u ! \ n " ) ;
exit (1); /∗ e x i t ( ) termine t ou t ∗/
}
i n t main ( i n t a r g c , char ∗ a r g v [ ] ) {
i n t p r o p o s i t i o n = N_INF − 1 ;
p t h r e a d _ t t1 , t 2 ;
/ ∗ Jeu c l a s s i q u e du nombre m y s t e r e ∗ /
p t h r e a d _ m u t e x _ l o c k (& mutex ) ; / ∗ P r o t e c t i o n du t e s t du w h i l e ∗ /
while ( p r o p o s i t i o n != m ys tere ) {
p t h r e a d _ m u t e x _ u n l o c k (& mutex ) ; / ∗ F in de p r o t e c t i o n du t e s t ∗ /
p r i n t f ( " Proposition ?\ n" ) ;
s c a n f ( " %d " ,& p r o p o s i t i o n ) ;
i f ( pro p os i t i on > mystere )
p r i n t f ( " Trop g r a n d ! \ n " ) ;
else {
i f ( p ro po s i ti o n < mystere )
p r i n t f ( " Trop p e t i t ! \ n " ) ;
else
break ;
}
p t h r e a d _ m u t e x _ l o c k (& mutex ) ; / ∗ P r o t e c t i o n du t e s t ∗ /
}
p r i n t f ( " Gagne ! \ n " ) ;
return 0;
}