Faculté des Sciences de Tunis
Département des Sciences de l'Informatique
Système d’exploitation
TP – Threads Posix
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 particuliè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 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 dynamiquement 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.
Création
#include <pthread.h>
int pthread_create(
pthread_t *p_tid, // pointeur sur identité du thread
const pthread_attr_t *p_attr, //NULL attribut par défaut
void*(*fonction)(void *), // Fonction exécuté par le thread
void *arg); //paramètres de la fonction
La primitive pthread_create crée une nouvelle activité 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 argument 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.
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 [Link]
(par exemple 1234.5). Deux primitives sont dédiées à la manipulation des identités des activités :
#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal (pthread_t tid_1, pthread_t tid_2) ;
• pthread_self() retourne l’identificateur du thread courant dans le processus courant (le tid).
• pthread_equal(tid_1,tid_2) retourne 0 si les deux identités transmises en argument sont
identiques et une valeur non nulle sinon.
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 automatiquement 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 :
#include <pthread . h>
void pthread_exit ( void ∗ p_status) ;
int pthread_detach ( pthread_t tid ) ;
• pthread_exit(p_status) termine l’activité qui l’a appelée en indiquant une valeur de retour à
l’adresse p_status. Cette primitive ne peut jamais retourner dans le thread qui l’a appelée.
• pthread_detach(tid) indique au système qu’il pourra récupérer les ressources allouées au thread
d’identifiant tid lorsqu’il terminera ou qu’il peut récupérer les ressources s’il est déjà terminé. Cette
primitive ne provoque pas la terminaison du thread appelant. Elle retourne 0 en cas de succès, un code
d’erreur sinon. Exercices
Synchronisation avec pthread_join
Lorsque nous créons des threads puis nous laissons continuer par exemple la fonction main, nous prenons le
risque de terminer le programme complètement sans avoir pu exécuter les threads. Nous devons en effet
attendre que les différents threads créés se terminent. Pour cela, il existe la fonction :
int pthread_join (pthread_t th, void ** thread_return);
Ses arguments sont dans l'ordre :
Le thread à attendre.
La valeur de retour de la fonction du thread th.
L'appel de cette fonction met en pause l'exécution du thread appelant jusqu'au retour de la fonction. Si aucun
problème n'a eu lieu, elle retourne 0 (zéro) et la valeur de retour du thread est passé à l'adresse indiquée
(second argument) si elle est différente de NULL. En cas de problème, la fonction retourne une des valeurs
suivantes :
ESRCH : Aucun thread ne correspond à celui passé en argument.
EINVAL : Le thread a été détaché ou un autre thread attend déjà la fin du même thread.
EDEADLK : Le thread passé en argument correspond au thread appelant.
Travail à faire
Exercice 0 : Tester l’exemple du cours et vérifier l’exécution des threads en parallèle.
Exercice 1 : partage des données et terminaison
Ecrire le code dans lequel le programme principal crée plusieurs threads (exemple 3), chaque thread crée
affiche son identité.
Déclarer des variables locales et globales, entières et tester la visibilité des variables par les différents threads.
Exercice2 : Somme Matrice
Le programme suma-mat.c ci-dessous illustre l’utilisation des vecteurs de threads pour faire la somme de deux matrices
A et B de N éléments. La somme de chaque ligne est calculée avec des threads indépendants en parallèle. Les
composantes sont générées aléatoirement avec rand().
Le thread principal attendra la fin de tous les thread pour afficher le résultat final.
Exercice 3 : parallélisation de la multiplication matrice × vecteur
L’opération de multiplication d’une matrice par un vecteur est l’une des plus utiles en informatique (calcul
scientifique, infographie...). Il est très intéressant de la paralléliser pour en améliorer la performance. Elle est
réalisée simplement par les deux boucles montrées en Figure 1 où on voit comment le vecteur résultat y est
calculé à partir de la matrice A et du vecteur x. Plus précisément, on voit que le ieme` élément du vecteur y
est calculé à partir seulement de la ieme` ligne de la matrice A et de tout le vecteur x. Puisque A et x restent
constants, on peut calculer chaque élément du vecteur y indépendamment les uns des autres.
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. Le thread principal attendra la fin de tous les thread pour
afficher le résultat final
for (i=0; i<NB_LIGNES; i++)
for (j=0; j<NB_COLONNES; j++)
y[i] += A[i][j] * x[j];
(*)
(i,*)
( i)
. =
A x y