Cours complet sur les threads en C avec
explications détaillées
1. Introduction aux threads
Les threads sont des unités d'exécution légères au sein d'un processus. Ils permettent
l'exécution concurrente de tâches et sont essentiels pour exploiter efficacement les
systèmes multiprocesseurs.
2. Création et gestion des threads en C
2.1 Création d'un thread
c
Copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread_function(void *arg) {
printf("Thread exécuté\n");
return NULL;
}
int main() {
pthread_t thread;
int result = pthread_create(&thread, NULL, thread_function,
NULL);
if (result != 0) {
fprintf(stderr, "Erreur lors de la création du thread\n");
return 1;
}
printf("Thread créé avec succès\n");
pthread_exit(NULL);
}
Explication :
• Nous incluons <pthread.h> pour utiliser les fonctions de la bibliothèque POSIX
threads.
• thread_function est la fonction que notre thread exécutera.
• Dans main(), nous déclarons une variable pthread_t pour stocker l'identifiant du
thread.
• pthread_create() crée le thread. Ses arguments sont :
1. Un pointeur vers la variable pthread_t
2. Les attributs du thread (NULL pour les attributs par défaut)
3. La fonction que le thread exécutera
4. Les arguments de cette fonction (NULL ici car nous n'en passons pas)
• Nous vérifions si la création du thread a réussi.
• pthread_exit(NULL) termine le thread principal proprement.
2.2 Attente de la fin d'un thread (join)
c
Copy
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
printf("Attente de la fin du thread...\n");
pthread_join(thread, NULL);
printf("Thread terminé\n");
return 0;
}
Explication :
• Après avoir créé le thread, nous utilisons pthread_join() pour attendre sa fin.
• pthread_join() bloque l'exécution du thread appelant jusqu'à ce que le thread
spécifié se termine.
• Le premier argument est l'identifiant du thread à attendre.
• Le second argument est un pointeur pour récupérer la valeur de retour du thread (NULL
ici car nous ne l'utilisons pas).
3- Les six caractéristiques principales
des threads
3.1. Légèreté
La légèreté est l'une des caractéristiques fondamentales des threads, les distinguant des
processus complets.
• Définition : Les threads sont considérés comme "légers" car ils nécessitent moins de
ressources système pour leur création, gestion et destruction que les processus.
• Implications :
o Création rapide : La création d'un thread est généralement beaucoup plus rapide que
celle d'un processus.
o Changement de contexte efficace : Passer d'un thread à un autre au sein d'un même
processus est plus rapide que de changer de processus.
o Empreinte mémoire réduite : Chaque thread ne nécessite que sa propre pile et quelques
structures de contrôle, contrairement à un processus qui requiert un espace d'adressage
complet.
• Avantages :
o Permet la création d'un grand nombre de threads sans surcharger le système.
o Facilite la conception de programmes hautement concurrents et réactifs.
3.2. Partage de ressources
Le partage de ressources est une caractéristique clé qui définit la relation entre les
threads d'un même processus.
• Définition : Les threads d'un même processus partagent le même espace mémoire et les
mêmes ressources du processus parent.
• Ressources partagées :
o Code du programme (segment de texte)
o Données globales et statiques
o Heap (tas) pour l'allocation dynamique de mémoire
o Descripteurs de fichiers ouverts
o Signaux et gestionnaires de signaux
o Informations de comptabilité
• Implications :
o Communication facilitée : Les threads peuvent communiquer directement via la
mémoire partagée.
o Économie de mémoire : Moins de duplication des ressources comparé aux processus
multiples.
o Risques de conflits : Nécessité de synchronisation pour éviter les conditions de course.
• Avantages et défis :
o Permet une coopération efficace entre les threads.
o Réduit la consommation globale de ressources du système.
o Exige une gestion soigneuse pour éviter les problèmes de concurrence.
3.3. État d'exécution indépendant
Chaque thread maintient son propre état d'exécution, permettant une exécution
véritablement parallèle.
• Définition : Chaque thread possède ses propres ressources d'exécution indépendantes
au sein du processus.
• Composants de l'état d'exécution :
o Compteur de programme (PC) : Indique la prochaine instruction à exécuter.
o Registres : Stockent les données de travail actuelles du thread.
o Pile : Contient les variables locales et les informations de retour de fonction.
o État du thread : Prêt, en exécution, bloqué, etc.
• Implications :
o Exécution parallèle : Les threads peuvent s'exécuter simultanément sur différents cœurs
de processeur.
o Préemption : Le système d'exploitation peut interrompre et reprendre l'exécution de
chaque thread indépendamment.
• Avantages :
o Permet une véritable concurrence sur les systèmes multiprocesseurs.
o Facilite la conception de programmes réactifs et performants.
3.4. Concurrence
La concurrence est la capacité des threads à s'exécuter en parallèle ou de manière
entrelacée.
• Définition : Les threads d'un processus peuvent s'exécuter de manière concurrente, soit
en véritable parallélisme sur des systèmes multiprocesseurs, soit par entrelacement sur
un seul processeur.
• Types de concurrence :
o Parallélisme réel : Exécution simultanée sur des cœurs de processeur distincts.
o Pseudo-parallélisme : Entrelacement rapide de l'exécution des threads sur un seul cœur.
• Implications :
o Amélioration des performances : Utilisation efficace des ressources de calcul
disponibles.
o Complexité accrue : Nécessité de gérer les interactions entre threads concurrents.
• Avantages et défis :
o Permet d'exploiter pleinement les architectures multiprocesseurs.
o Peut conduire à des problèmes subtils de synchronisation et de timing.
3.5. Synchronisation
La synchronisation est cruciale pour gérer l'accès aux ressources partagées et
coordonner l'exécution des threads.
• Définition : Mécanismes permettant de coordonner l'exécution des threads et de
contrôler l'accès aux ressources partagées.
• Mécanismes de synchronisation :
o Mutex (exclusion mutuelle) : Protège les sections critiques du code.
o Sémaphores : Contrôle l'accès à un nombre limité de ressources.
o Variables de condition : Permet aux threads d'attendre que certaines conditions soient
remplies.
o Barrières : Synchronise un groupe de threads à un point donné de l'exécution.
• Problèmes de synchronisation :
o Conditions de course : Accès non contrôlé aux données partagées.
o Deadlocks : Situations où des threads s'attendent mutuellement, bloquant l'exécution.
o Starvation : Un thread est constamment privé d'accès à une ressource.
• Importance :
o Essentielle pour garantir la cohérence des données et le bon fonctionnement des
programmes multithreads.
o Requiert une conception soigneuse pour éviter les problèmes de performance et de
fiabilité.
3.6. Communication inter-threads
La communication entre threads est facilitée par le partage de mémoire mais nécessite
des mécanismes de coordination.
• Définition : Méthodes par lesquelles les threads échangent des informations et
coordonnent leurs actions.
• Méthodes de communication :
o Variables partagées : Utilisation directe de la mémoire partagée.
o Files de messages : Structures de données pour l'échange ordonné de messages.
o Signaux : Notifications asynchrones entre threads.
• Modèles de communication :
o Producteur-consommateur : Un thread produit des données, un autre les consomme.
o Lecteurs-écrivains : Gestion de l'accès concurrent à des ressources en lecture et écriture.
o Client-serveur : Un thread fournit des services à d'autres threads.
• Défis :
o Cohérence des données : S'assurer que les données partagées restent dans un état
cohérent.
o Éviter les interblocages : Gérer les dépendances circulaires dans la communication.
• Avantages :
o Permet une collaboration efficace entre les threads.
o Facilite la conception de systèmes complexes et réactifs.
Ces six caractéristiques - légèreté, partage de ressources, état d'exécution indépendant,
concurrence, synchronisation, et communication inter-threads - forment la base de la
compréhension et de l'utilisation efficace des threads dans la programmation moderne.
4. Explication des points clés
4.1 Multiples threads
c
Copy
#define NUM_THREADS 5
void *print_hello(void *thread_id) {
long tid = (long)thread_id;
printf("Hello from thread %ld\n", tid);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
for(long t=0; t<NUM_THREADS; t++){
printf("Creating thread %ld\n", t);
pthread_create(&threads[t], NULL, print_hello, (void *)t);
}
pthread_exit(NULL);
}
Explication :
• Nous définissons NUM_THREADS pour spécifier le nombre de threads à créer.
• print_hello est la fonction exécutée par chaque thread. Elle prend un void* comme
argument et le convertit en long pour l'identifier.
• Dans main(), nous créons un tableau de pthread_t pour stocker les identifiants des
threads.
• La boucle for crée NUM_THREADS threads, en passant l'index t comme identifiant à
chaque thread.
• Nous convertissons t en void* pour le passer comme argument à pthread_create().
• pthread_exit(NULL) à la fin de main() permet aux threads de continuer leur
exécution même après que la fonction principale se termine.
4.2 Ressources partagées
c
Copy
#include <pthread.h>
#include <stdio.h>
int shared_variable = 0;
pthread_mutex_t mutex;
void *increment_shared(void *arg) {
for(int i=0; i<1000000; i++) {
pthread_mutex_lock(&mutex);
shared_variable++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&thread1, NULL, increment_shared, NULL);
pthread_create(&thread2, NULL, increment_shared, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Valeur finale: %d\n", shared_variable);
pthread_mutex_destroy(&mutex);
return 0;
}
Explication :
• shared_variable est une ressource partagée entre les threads.
• pthread_mutex_t mutex déclare un mutex pour protéger l'accès à la variable
partagée.
• Dans increment_shared(), nous utilisons pthread_mutex_lock() et
pthread_mutex_unlock() pour créer une section critique autour de l'incrémentation
de shared_variable.
• pthread_mutex_init() initialise le mutex avant de créer les threads.
• Nous créons deux threads qui exécutent increment_shared().
• pthread_join() attend que les deux threads se terminent.
• pthread_mutex_destroy() libère les ressources du mutex à la fin.
4.3 Exécution indépendante
c
Copy
#include <pthread.h>
#include <stdio.h>
void *count_to_ten(void *arg) {
int local_counter = 0;
for(int i=0; i<10; i++) {
local_counter++;
printf("Thread %ld: compteur = %d\n", (long)arg,
local_counter);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, count_to_ten, (void *)1);
pthread_create(&thread2, NULL, count_to_ten, (void *)2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
Explication :
• count_to_ten() utilise une variable locale local_counter.
• Chaque thread a sa propre copie de local_counter sur sa pile d'exécution.
• Les threads sont créés avec des identifiants différents (1 et 2) passés comme arguments.
• L'exécution montre que chaque thread incrémente son propre compteur
indépendamment.
4.4 Création légère
c
Copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <time.h>
#define NUM_TASKS 10000
void *thread_function(void *arg) {
return NULL;
}
int main() {
clock_t start, end;
double cpu_time_used;
// Mesure du temps pour la création de threads
start = clock();
for(int i=0; i<NUM_TASKS; i++) {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
pthread_join(thread, NULL);
}
end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("Temps pour créer %d threads: %f secondes\n", NUM_TASKS,
cpu_time_used);
// Mesure du temps pour la création de processus
start = clock();
for(int i=0; i<NUM_TASKS; i++) {
pid_t pid = fork();
if(pid == 0) {
exit(0);
} else {
wait(NULL);
}
}
end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("Temps pour créer %d processus: %f secondes\n",
NUM_TASKS, cpu_time_used);
return 0;
}
Explication :
• Ce programme compare le temps nécessaire pour créer et terminer des threads vs des
processus.
• clock() est utilisé pour mesurer le temps d'exécution.
• Pour les threads :
o Nous créons et joignons immédiatement chaque thread.
• Pour les processus :
o fork() crée un nouveau processus.
o Le processus enfant se termine immédiatement avec exit(0).
o Le processus parent attend la fin de l'enfant avec wait(NULL).
• Le temps d'exécution pour chaque méthode est calculé et affiché.
• Généralement, la création de threads est significativement plus rapide que celle des
processus.
4.5 Communication facilitée
c
Copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5
int shared_sum = 0;
pthread_mutex_t mutex;
void *add_to_sum(void *thread_id) {
int tid = *((int*)thread_id);
int local_sum = 0;
for(int i=0; i<1000; i++) {
local_sum += i;
}
pthread_mutex_lock(&mutex);
shared_sum += local_sum;
printf("Thread %d a ajouté %d à la somme totale\n", tid,
local_sum);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
pthread_mutex_init(&mutex, NULL);
for(int i=0; i<NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, add_to_sum, (void
*)&thread_ids[i]);
}
for(int i=0; i<NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Somme finale: %d\n", shared_sum);
pthread_mutex_destroy(&mutex);
return 0;
}
Explication :
• shared_sum est une variable globale partagée entre tous les threads.
• Chaque thread calcule une somme locale, puis l'ajoute à shared_sum.
• Le mutex protège l'accès à shared_sum pour éviter les conditions de course.
• Dans main(), nous créons NUM_THREADS threads, chacun avec son propre ID.
• Après que tous les threads ont terminé, nous affichons la somme finale.
4.6 Parallélisme
c
Copy
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 4
#define ARRAY_SIZE 1000000
int array[ARRAY_SIZE];
long long partial_sums[NUM_THREADS] = {0};
void *sum_array_portion(void *arg) {
int thread_id = *((int*)arg);
int portion_size = ARRAY_SIZE / NUM_THREADS;
int start = thread_id * portion_size;
int end = (thread_id == NUM_THREADS - 1) ? ARRAY_SIZE : start +
portion_size;
for(int i=start; i<end; i++) {
partial_sums[thread_id] += array[i];
}
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// Initialisation du tableau
for(int i=0; i<ARRAY_SIZE; i++) {
array[i] = rand() % 100;
}
// Création des threads
for(int i=0; i<NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, sum_array_portion, (void
*)&thread_ids[i]);
}
// Attente de la fin des threads
for(int i=0; i<NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// Calcul de la somme totale
long long total_sum = 0;
for(int i=0; i<NUM_THREADS; i++) {
total_sum += partial_sums[i];
}
printf("Somme totale: %lld\n", total_sum);
return 0;
}
Explication :
• Ce programme illustre le parallélisme en calculant la somme des éléments d'un grand
tableau.
• array est le tableau à sommer, initialisé avec des valeurs aléatoires.
• partial_sums stocke les sommes partielles calculées par chaque thread.
• sum_array_portion() est la fonction exécutée par chaque thread :
o Elle calcule les indices de début et de fin de la portion du tableau à traiter.
o Elle somme les éléments de sa portion et stocke le résultat dans partial_sums.
• Dans main() :
o Nous initialisons le tableau avec des valeurs aléatoires.
o Nous créons NUM_THREADS threads, chacun traitant une portion du tableau.
o Après que tous les threads ont terminé, nous additionnons les sommes partielles pour
obtenir la somme totale.
Ce programme démontre comment le parallélisme peut être utilisé pour accélérer les
calculs sur de grandes quantités de données en répartissant le travail entre plusieurs
threads.