Un thread, c’est une suite d’instructions que l’ordinateur exécute étape par étape, à l’intérieur d’un programme. Un programme simple n’a qu’un seul thread.
On parle de programme multi-thread quand il utilise au moins deux threads qui peuvent fonctionner en même temps, chacun faisant une tâche différente.
Dans un programme multithread, l’exécution des threads peut être :
Concurrente
Lorsqu’on a plusieurs threads qui s’exécute en même temps, mais qu’en fait le processeur alterne entre les deux.
Parallèle
Lorsque les threads s’exécutent en même temps, chacun sur un cœur de processeur différent.
Remarque
Chaque thread d’un processus possède son propre contexte : son identifiant unique, sa pile d’exécution ou stack, son pointeur d’instruction, son registre de processeur. Mais comme tous les threads font partie du même processus, ils partagent le même espace d’adressage virtuel :
le même code,
les mêmes bibliothèques partagées,
les mêmes descripteurs de fichiers ouverts
Puisque le contexte d’un thread est plus petit que celui d’un processus il est donc plus rapide pour le système de créer un thread que de créer un processus.
Les threads n’ont pas la hiérarchie père fils ainsi un thread ne dépend pas de celui qui l’a créé. Seul le premier thread créé à une distinction. Ainsi, :
n’importe quel thread peut tuer
n’importe quel thread peut attendre la fin d’un autre au sein d’un même processus
En langage C on utilise la bibliothèque <pthread.h>.
Attention
Pour pouvoir compiler il ne faut pas oublier de lier la bibliothèque avec -pthread.
#include<pthread.h>
Ainsi la ligne de compilation :
gcc -pthread fichier.c
Création d’un thread
Il est possible de créer un thread depuis n’importe quel thread du programme avec pthread_create :
*thread pointeur vers l’identifiant du thread créé
*attr pointeur vers une structure d’attribut du thread
⟹NULL pour les attributs par défaut
*(*fct)(void *) la fonction que le thread exécutera. Prend void* en argument et renvoie un void*Quand le thread arrive à la fin de la fonction, il aura terminé ses tâches.
*arg arguments à passer à la fonction fct
Les renvois possible :
0 le thread a bien été créé
>0 code d’erreur
Remarque
En linux, pthread_t est un entier long unsigned long qui permet d’avoir jusqu’à 4 milliards de threads en même temps.
Dans les autres système, c’est encore flou et en évolution…
EXEMPLE création de 5 threads.
#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#define NB_THREAD 5void* routine_thread(void* arg) { int numThread = *(int*)arg; printf("Bonjour depuis le thread %d\n", numThread); return NULL;}int main(void) { pthread_t threads[NB_THREAD]; int indices[NB_THREAD]; for (int i = 0; i < NB_THREAD; i++) { indices[i] = i; int retour = pthread_create(&threads[i], NULL, routine_thread, &indices[i]); if (retour != 0) { fprintf(stderr, "%s\n", strerror(retour)); exit(1); } } for (int i = 0; i < NB_THREAD; i++) { pthread_join(threads[i], NULL); } return 0;}
Bonjour depuis le thread 0
Bonjour depuis le thread 1
Bonjour depuis le thread 2
Bonjour depuis le thread 3
Bonjour depuis le thread 4
On utilise un tableau statique indice[NB_THREAD] pour stocker une copie stable de i.
Si on utilise pas le tableau :
for (int i = 0; i < NB_THREAD; i++) { pthread_create(&threads[i], NULL, routine_thread, &i); // ❌ ERREUR}
Ici on a toujours la même adresse pour i donc les threads reçoivent le même pointeur.
Attributs d’un thread
Un thread possède plusieurs attributs regroupés dans une structure de donnée spécifique qui dépend du système d’exploitation.
Elle est représentée par le type pthread_attr_t.
Tip
Si les attributs par défaut suffisent, on passe un type vide à la création du thread.
ptread_create(-, NULL, -, -);
Dans le cas contraire, il faudra définir la structure puis la remplir.
Fin d’un thread
Pour terminer un thread on utilise la fonction suivante :
valeur de retour optionnelle que les autres threads peuvent récupérer avec la fonction pthread_join.
Pourquoi utiliser pthread_exit ?
On utilise pthread_exit pour :
quitter un thread sans terminer le programme
retourner une valeur aux autres threads
terminer un thread lorsque cela n’arrive pas à la fin de sa fonction
EXEMPLE terminer un thread, puis récupérer la valeur qu’il a retourné.
#include <pthread.h>#include <stdio.h>#include <stdlib.h>void* routine_thread(void* arg) { int* result = malloc(sizeof(int)); *result = 42; pthread_exit((void*)result); // équivaut à : return result;}int main(void) { pthread_t t; int* retour; pthread_create(&t, NULL, routine_thread, NULL); pthread_join(t, (void**)&retour); printf("Valeur retournée par le thread : %d\n", *retour); free(retour); // Libérer la mémoire return 0;}
Valeur retournée par le thread : 42
Capturer la fin d’un thread
La fonction que nous venons d’utiliser pthread_join est en fait une fonction bloquante, en gros tant que le thread spécifié en argument n’a pas terminé, on attend.
Signature de la fonction
thread l’identifiant du thread que l’on veut attendre.
retour un pointeur vers un pointeur qui reçoit la valeur retournée par le thread via pthread_exit.
Tip
Si on ne veut pas récupérer de valeur :
pthread_join(-, NULL);
EXEMPLE completOn reprend l’exemple précédant en ajoutant des vérifications
#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include<string.h>void* routine_thread(void* arg) { int* result = malloc(sizeof(int)); *result = 42; return result;}int main(void) { pthread_t t; int* retour; // Variable de stockage du retour du thread int r; // Le thread a bien été créé ? if(r=pthread_create(&t, NULL, routine_thread, NULL)!=0){ fprintf(stderr, "%s\n", strerror(r)); exit(1); } // On attends jusqu'à récupérer une valeur pthread_join(t, (void**)&retour); // Le retour existe ? if(retour != PTHREAD_CANCELED){ printf("Valeur retournée par le thread : %d\n", *retour); } free(retour); // Libérer la mémoire return 0;}
Détachement d’un thread
On peut créer des threads qui s’auto libèrent à la fin de leur exécution. Par contre, on ne peut plus les attendre avec pthread_join.
En gros, quand on a pas besoin d’attendre un thread on peut le détacher, il disparaît automatiquement du système quand il termine puis il libère immédiatement des ressources privées donc pas d’état zombie.
#include <pthread.h>#include <stdio.h>#include <unistd.h>void* routine_thread(void* arg) { printf("Thread détaché : je travaille...\n"); sleep(2); printf("Thread détaché : terminé.\n"); return NULL;}int main() { pthread_t thread; pthread_create(&thread, NULL, routine_thread, NULL); pthread_detach(thread); // ✅ On détache le thread printf("Main : pas besoin de l’attendre.\n"); sleep(3); // Attendre un peu pour voir le thread travailler return 0;}
Main : pas besoin de l'attendre.
Thread détaché : je travaille...
Thread détaché : terminé.
Problème : Émancipation d’un thread
L’émancipation c’est le fait de libérer un thread donc avec pthread_detach. Une fois cela fait :
le thread devient invisible aux yeux du programme
il se nettoiera seul à la fin de son exécution
le programme principal n’a aucun moyen de savoir quand il se termine ni de récupérer sa valeur de retour.
Il peut donc se produire des comportements imprévisible si on utilise mal pthread_detach :
Le thread détaché peut ne pas avoir le temps de finir. Si le programme finit avant lui il est brutalement stoppé.
EXEMPLE
pthread_t t;pthread_create(&t, NULL, fct, NULL);pthread_detach(t);return 0; // Le thread peut ne pas avoir le temps finir !
Warning
Si un thread n’est ni détaché ni rejoint, il devient zombie, en gros ça veut dire qu’il a terminé, mais que ses ressources ne sont pas libérées. Cela peut provoquer des fuites de mémoires.
Tip
On utilise pthread_detach quand :
On n’a pas besoin du résultat du thread
On ne veut pas l’attendre
One est certain qu’il n’interagit plus avec le reste du programme une fois lancé.
Thread démarré
Travail 0
Travail 1
Travail 2
Travail 3
Thread annulé
Ici le thread affiche un message 10 fois.
Le programme fait une demande d’annulation après 3 secondes et puisqu’on re définit un point d’annulation à la fin de chaque boucle, au moment de la demande, le thread concerné va en fait faire le tour de boucle courant puis s’annuler lorsqu’il rencontrer pthread_testcancel. Cette instruction c’est comme si le thread se demandais si on lui a envoyé une demande d’annulation.
Thread démarré
Travail 0
Travail 1
Travail 2
Travail 3
Travail 4
Travail 5
Travail 6
Travail 7
Travail 8
Travail 9
Thread terminé normalement
Thread terminé normalement
Ici le thread vas aller jusqu’au bout de ses tâches.
Fonction d’initialisation partagée
La fonction d’initialisation partagée est un mécanisme utile pour s’assurer qu’un bloc de code d’initialisation n’est exécuté une seule fois, même si plusieurs threads y accèdent en même temps.
On utilise la fonction suivante :
once_control c’est un contrôle de synchronisation statique
généralement PTHREAD_ONCE_INIT
Ce contrôle doit être partagé par tous les threads qui doivent appeler routine
routine pointeur vers une fonction sans argument et sans valeur de retour
EXEMPLE
#include <pthread.h>#include <stdio.h>pthread_once_t once_control = PTHREAD_ONCE_INIT;void initialisation_unique() { printf("Initialisation partagée effectuée.\n");}void* routine(void* arg) { pthread_once(&once_control, initialisation_unique); printf("Thread %ld continue...\n", (long)arg); return NULL;}int main() { pthread_t threads[3]; for (long i = 0; i < 3; i++) { pthread_create(&threads[i], NULL, routine, (void*)i); } for (int i = 0; i < 3; i++) { pthread_join(threads[i], NULL); } return 0;}
Au premier appel avec un argument once_control donné, on appelle la fonction routine et il y a modification de la variable once pour enregistrer le fait que l’initialisation a été réalisée.
Les appels suivants avec ce même argument once_init son ignorés.
Zone d’exclusion mutuelle
La zone d’exclusion mutuelle (mutex, = MUTual EXclusion) est un concept fondamental en programmation concurrente. On utilise ce concept pour gérer l’accès concurrent des ressources partagées afin d’éviter les conditions de course et d’assurer l’intégralité des données.
Un programme multithread implique :
des sous-programmes concurrents
mémoire commune
Il faut donc organiser les accès pour éviter que plusieurs threads utilisent une ressource en même temps.
En langage C, on synchronise l’accès aux ressources partagées dans un programme multithread principalement à l’aide des verrous MUTEX.
Initialisation d’un mutex
On peut initialiser un mutex de deux manières :
statique
On initialise le mutex à la compilation avec la constante PTHREAD_MUTEX_INITIALIZER
A utiliser de manière modéré car consommateur de temps cpu par rapport à des attentes conditionnelles.
EXEMPLE
#include <stdio.h>#include <stdlib.h>#include <pthread.h>#include <unistd.h>#include <errno.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* thread_fonction(void* arg) { int thread_num = *((int*)arg); if (pthread_mutex_trylock(&mutex) == 0) { printf("Thread [%d] : verrou acquis\n", thread_num); sleep(2); // Simule un travail pthread_mutex_unlock(&mutex); printf("Thread [%d] : verrou relâché\n", thread_num); } else { printf("Thread [%d] : verrou déjà pris, ne bloque pas\n", thread_num); } return NULL;}int main() { pthread_t t1, t2; int id1 = 1, id2 = 2; pthread_create(&t1, NULL, thread_fonction, &id1); sleep(1); // Laisse le temps à t1 de prendre le verrou pthread_create(&t2, NULL, thread_fonction, &id2); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0;}
Thread [1] : verrou acquis
Thread [2] : verrou déjà pris, ne bloque pas
Thread [1] : verrou relâché
Exemple complet MUTEX
Nous on veut que n threads incrémente un 10.000 fois compteur. Ainsi la valeur final de ce compteur sera nThread×10000.
Mais en fait, pourquoi on utilise un mutex pour faire ça ? On veut éviter le phénomène appelé condition de course.
Une condition de course survient lorsqu’au moins deux thread accèdent en même temps à une ressource partagée et que :
au moins un de ces accès et une écriture et
il n’y a pas de mécanisme de synchronisation (mutex par exemple)
Cela signifie que le résultat du programme dépend de l’ordre d’exécution imprévisible des threads, ce qui peut entraîner des comportements incorrects.
⚠️ Dans notre exemple, sans mutex :
Voici le code critique :
compteur++;
Cette instruction a l’air simple, mais elle se traduit en plusieurs étapes machines :
Lire la valeur de compteur (ex: 10)
Incrémenter localement (→ 11)
Écrire la nouvelle valeur (11) dans compteur
🧨 Que se passe-t-il si deux threads font ça en même temps ?Imaginons :
Thread1 lit compteur = 10
Thread2 lit aussi compteur = 10
Thread1 incrémente et écrit 11
Thread2 incrémente sa copie de 10 et écrit aussi 11
Ainsi la valeur de compteur après l’incrémentation sera 11 au lieu de 12.
🔴 Résultat attendu : 12
🔴 Résultat obtenu : 11
⛔ 1 incrément perdu
Remarque
Plus il y a de threads et d’opérations, plus les erreurs deviennent fréquentes
#include <stdio.h>#include <stdlib.h>#include <pthread.h>#define NB_THREADS 4#define NB_INCREMENTS 100000int compteur = 0; // Variable partagéepthread_mutex_t mutex; // Mutex pour protéger compteurvoid* incrementer(void* arg) { int id = *((int*)arg); for (int i = 0; i < NB_INCREMENTS; i++) { pthread_mutex_lock(&mutex); // Verrouillage compteur++; pthread_mutex_unlock(&mutex); // Déverrouillage } printf("Thread %d terminé\n", id); return NULL;}int main() { pthread_t threads[NB_THREADS]; int ids[NB_THREADS]; // Initialisation du mutex if (pthread_mutex_init(&mutex, NULL) != 0) { perror("pthread_mutex_init"); return 1; } // Création des threads for (int i = 0; i < NB_THREADS; i++) { ids[i] = i + 1; if (pthread_create(&threads[i], NULL, incrementer, &ids[i]) != 0) { perror("pthread_create"); return 1; } } // Attente de la fin des threads for (int i = 0; i < NB_THREADS; i++) { pthread_join(threads[i], NULL); } // Destruction du mutex pthread_mutex_destroy(&mutex); printf("Compteur final attendu : %d\n", NB_THREADS * NB_INCREMENTS); printf("Compteur final obtenu : %d\n", compteur); return 0;}
Objectif : Un certain nombre de thread incrémentent simultanément une variable compteur, pour éviter les conflits d’accès, un mutex est utilisé pour synchroniser l’accès à la variable compteur.
Déclaration des constantes NB_THREAD et NB_INCREMENTS
Déclaration de la variable partagée entre les threads
On initialise le verrou qui vas assurer qu’un seul thread à la fois modifie la variable compteur.
Chaque threads vas exécuter ma fonction
void* incrementer(void* arg)
Puisque chaque thread reçoit un argument arg contenant son id (ici de 1 à4) alors on récupère ce dernier puis on le caste en le déréférençant.
Chaque thread vas ensuite effectuer 10000 incréments
Le mutex est utilisé pour verrouillé l’accès au compteur pendant l’incrémentation, pour éviter qu’un thread intervienne au même moment.
Données privées d’un thread ?
On l’a pas vu en TP donc je pense que ça tombera pas.