Académique Documents
Professionnel Documents
Culture Documents
M3101 Système S3
2015 / 2016
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 et d’exclusion mutuelle.
1 Threads
Les threads, qu’on appelle aussi processus légers, sont des unités d’exécution qui opèrent dans
le contexte d’un processus. Un processus peut contenir plusieurs threads qui exécutent tous le même
programme et partagent la même mémoire. Plus précisément, les segments de code (le programme),
de données (les variables globales) et le tas (les allocations dynamiques) sont partagés entre les diffé-
rentes threads d’un même processus. Chaque thread dispose de sa propre pile, ce qui leur permet de
poursuivre les chemins d’exécution différents, mais comme ces piles habitent la mémoire commune,
une variable locale dans la pile d’un thread peut être lue et modifiée par un autre thread s’il en connaît
l’adresse.
La mémoire partagée permet à des threads d’un processus d’échanger des données sans recourir
à des outils de communication entre processus tels que tubes ou sockets. La création d’un nouveau
thread est aussi moins couteuse que la création d’un nouveau processus. En effet, même si le noyau
ne duplique pas le contenu de mémoire d’un processus au moment de fork(), il doit tout de même
faire une copie de sa table de pages. Pour la même raison, le changement de contexte entre deux
threads d’un processus est plus rapide qu’entre deux processus.
Cependant, la programmation avec des threads est une affaire délicate : l’accès simultané aux
variables partagées peut amener à des erreurs d’interférence qui sont à la fois
— difficiles à détecter, car l’apparition ou non de l’erreur dépend de décisions d’ordonnancement
ainsi que de détails d’architecture dans le cas d’une machine multiprocesseur ;
— difficiles à analyser, car les exécutions consécutives d’un même programme ne se comportent
pas de la même façon, et d’ailleurs le débogage même — l’addition du code de debug ou
l’utilisation d’un débogueur — peut empêcher à l’erreur de se manifester (ce cauchemar de
programmeur à reçu le nom de Heisenbug) ;
— difficiles à corriger, car la gestion correcte d’accès aux données partagées peut nécessiter des
modifications majeures dans le code du programme, jusqu’à la réimplémentation complète
des parties concernées.
Ainsi, la conception et le développement d’une application multi-threadée exige que le programmeur
soit parfaitement conscient des problèmes liés au travail en mémoire partagée et à l’aise avec les
solutions telles que l’exclusion mutuelle, les moniteurs, les structures de données adaptées, etc.
Dans ce cours nous allons utiliser les threads POSIX (appelés pthreads) et leur implémentation
actuelle dans Linux.
Travaux Dirigés no 4 Threads 2/7
La primitive pthread_create() crée un nouveau thread et écrit son identifiant dans thread_id
(il s’agît d’une valeur opaque 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.
L’argument routine est un pointeur sur la fonction qui sera exécutée par le thread. Cette fonction,
qu’on appelle routine de départ (start routine), retourne un void* et prend un unique argument de
type void*. Enfin, le dernier argument arg correspond à l’argument transmis à la fonction routine.
Cette primitive renvoie 0 en cas de succès, un code d’erreur sinon.
Le nouveau thread a le même PID et PPID que le thread créateur. Il partage également ses permis-
sions (UID, GID, EUID, EGID), ses fichiers ouverts, ses gestionnaires de signaux (mais non pas la
liste de signaux bloqués), son répertoire courant, ses variables d’environnement et d’autres propriétés
(voir man 7 pthreads pour la liste complète).
Chaque thread a un identifiant unique de type pthread_t. Pour obtenir l’identifiant d’un thread
ou comparer deux identifiants on utilise les primitives suivantes :
#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t tid1, pthread_t tid2);
La primitive pthread_equal() renvoie une valeur non-nulle si tid1 et tid2 sont égaux et 0 sinon.
3 Terminaison de threads
Tous les threads d’un processus prennent fin si un des threads appelle exit() ou _exit(), ou bien
si le thread initial (celui qui existait seul dans le processus avant tout appel à pthread_create())
retourne de la fonction main(). Un thread seul prend fin automatiquement quand sa routine de dé-
part (la fonction passée en argument de pthread_create()) retourne ou quand le thread appelle la
primitive pthread_exit() :
#include <pthread.h>
void pthread_exit(void* retval);
qui prend en argument la valeur de retour du thread. La primitive pthread_exit() ne peut jamais
retourner dans le thread appelant. La valeur de retour peut être un entier converti en void* : par
exemple, on peut renvoyer ((void*) 1) de la routine de départ.
Tout thread est par défaut considéré joignable. Cela veut dire qu’à la terminaison de ce thread
ses ressources ne sont pas libérées avant qu’un autre thread n’appele la primitive pthread_join() :
#include <pthread.h>
int pthread_join(pthread_t thread_id, void** retval);
La primitive suspend l’exécution du thread appelant jusqu’à la fin du thread d’identifiant thread_id.
Si le thread d’identifiant thread_id est déjà terminé, elle retourne immédiatement. En cas de succès,
elle renvoie 0 et place à l’adresse *retval la valeur de retour du thread attendu. En cas d’échec, elle
renvoie un code d’erreur. La primitive pthread_join() joue ainsi le même rôle pour les threads que
la primitive waitpid() pour les processus.
Il est possible de déclarer un thread non-joignable, ou détaché, en utilisant pthread_detach() :
#include <pthread.h>
int pthread_detach(pthread_t thread_id);
Cette primitive indique au noyau qu’il pourra récupérer les ressources allouées au thread thread_id
lorsqu’il terminera (immédiatement s’il est déjà terminé). Un appel de pthread_join() sur un thread
détaché ou déjà attendu par un autre thread renvoie le code d’erreur EINVAL.
4 Exclusion mutuelle
Un outil principal de synchronisation entre threads est l’objet d’exclusion mutuelle, ou mutex.
Les mutex permettent d’assurer que plusieurs threads ne peuvent pas accéder aux valeurs partagées
en même temps.
À tout moment donné, un mutex est soit pris par aucun thread (on dit déverrouillé), soit pris par
un et un seul thread (on dit verrouillé). Tout thread qui essaie de prendre un mutex déjà verrouillé
doit attendre jusqu’à ce que le mutex se libère.
On manipule les mutex à l’aide des quatre primitives fondamentales suivantes qui retournent
toutes 0 en cas de succès et un code d’erreur sinon :
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, NULL);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
5 Exercices
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 identifiant :
#define NB_THREADS 3
pthread_t tid[NB_THREADS];
int thread_execute = 0;
int main() {
int i;
for (i = 0; i < NB_THREADS; i++) {
if (pthread_create(&tid[i], NULL, routine, (void *) &i) != 0)
{ fprintf(stderr, "Erreur création thread numéro %d.\n", i); exit(1); }
}
printf("Thread initial d'ID %lu\n", pthread_self());
if (thread_execute)
printf("Des threads annexes ont été exécutés.\n");
else
printf("Aucun thread annexe n'a été exécuté.\n");
return 0;
}
Énumérez toutes les variables et dites par quels threads elles sont directement utilisables.
Expliquez l’exécution suivante où le numéro de chaque thread est le même. Proposez une solution.
Thread numéro 3, ID 3084860304
Thread numéro 3, ID 3076467600
Thread numéro 3, ID 3068074896
Thread initial d'ID 3084863168
Des threads annexes ont été exécutés.
Expliquez l’exécution suivante où aucun thread n’a réalisé son affichage. Proposez une solution.
Thread initial d'ID 3084601024
Aucun thread annexe n'a été exécuté.
(i, ∗)
(i)
A x y
Proposez un programme utilisant les threads pour le calcul du vecteur y tel que chaque élément
de ce vecteur soit calculé en parallèle par rapport aux autres.
Exercice 3 : Synchronisation de threads. Le but de cet exercice est d’écrire un programme dans
lequel le thread initial et un thread annexe, chacun de leur côté, incrémentent une variable partagée
initialisée à 0. Le thread initial affiche la valeur finale de la variable partagée avant de terminer.
Discutez les risques d’un manque de synchronisation dans un tel programme. Écrivez un programme
réalisant ces opérations de manière sûre (avec les synchronisations adéquates).
Correction. Le programme est relativement simple : il faut commencer par écrire le code du jeu
du nombre mystère habituel. Ensuite on intègre un premier thread simple pour le temps maximal du
jeu. Quand le temps maximum est écoulé, ce thread peut quitter tout le programme par un appel à
la primitive exit(). Enfin on ajoute la dimension « fuyante » par un nouveau thread. Le besoin en
synchronisation est centré sur le nombre mystère. On utilise un mutex pour assurer qu’un seul thread
pourra accéder en lecture comme en écriture au nombre mystère : le thread initial doit tester si la
proposition du joueur est correcte (accès en lecture), et le thread annexe modifie ce nombre (accès en
écriture). Lorsque le joueur a gagné, le thread initial termine, mettant ainsi immédiatement fin aux
autres threads.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define N_INF 0
#define N_SUP 200
#define X_MAX 50
#define T_INF 5
#define T_SUP 10
#define TIMEOUT 40