Cadre de travail
Vous utiliserez votre dépôt Git existant OS-Nom-Prenom pour organiser ce TD. L’objectif est de regrouper tous vos travaux pratiques de système dans une structure cohérente.
Organisation attendue :
C-Unix-Thread-Nom-Prenom /
├─ .gitignore
├─ README.md
├─ Makefile # pour compiler tous les programmes
├─ calcul_sequentiel.c
├─ calcul_parallele.c
├─ compteur_dangereux.c
├─ compteur_securise.c
├─ producteur_consommateur.c
├─ test_mutex.c
├─ test_semaphore.c
├─ cache_rwlock.c
└─ test_rwlock.c
Makefile de base :
CC = gcc
CFLAGS = -Wall -Wextra -pthread -g
LDFLAGS = -pthread
ALL = calcul_sequentiel calcul_parallele compteur_dangereux compteur_securise \
test_mutex producteur_consommateur test_semaphore cache_rwlock test_rwlock
all: $(ALL)
%: %.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
rm -f $(ALL)
Parallélisation d’un calcul
Dans cet exercice, vous allez transformer un algorithme séquentiel en version parallèle pour comprendre les bénéfices de la concurrence sur les opérations en lecture seule. L’idée fondamentale est que lorsque plusieurs threads ne font que lire des données partagées sans les modifier, il n’y a aucun problème de concurrence. C’est le scénario idéal du multithreading : diviser le travail sans conflits : maximum de performances !
Le code suivant simule un traitement “intensif” sur un tableau de données. Chaque élément nécessite un calcul “coûteux” en CPU. Dans la version séquentielle, ces calculs se font un par un.
Créez calcul_sequentiel.c :
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#define TAILLE_DONNEES 10
#define MULTIPLICATEUR 1000
static const int DONNEES[TAILLE_DONNEES] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int main(void) {
printf("=== Calcul Séquentiel ===\n");
struct timespec debut, fin;
clock_gettime(CLOCK_MONOTONIC, &debut);
int64_t somme_total = 0;
// TODO: Ce code sera remplacé par une version parallèle
for (int i = 0; i < TAILLE_DONNEES; i++) {
int valeur = DONNEES[i];
// Calcul intensif simulé par des opérations mathématiques
int64_t resultat = 0;
for (int j = 0; j < MULTIPLICATEUR; j++) {
resultat += valeur * valeur + valeur;
usleep(20);
}
somme_total += resultat;
printf("Traitement de %d terminé : %ld\n", valeur, resultat);
}
clock_gettime(CLOCK_MONOTONIC, &fin);
long duree = (fin.tv_sec - debut.tv_sec) * 1000 +
(fin.tv_nsec - debut.tv_nsec) / 1000000;
printf("Résultat total : %ld\n", somme_total);
printf("Durée : %ld ms\n", duree);
return 0;
}
Observations importantes :
- Le tableau
DONNEESest déclaréconst: il ne change jamais - Chaque calcul est indépendant : le traitement de l’élément
in’affecte pas l’élémentj - La variable
somme_totalaccumule les résultats : a faire en dehors des threads, a la fin
Compilation et exécution :
make calcul_sequentiel && ./calcul_sequentiel
Structure d’un thread
En C, on utilise, soit les fonctions systeme directement (windows, linux), soit la bibliothèque POSIX threads (pthreads) qui encapsule les appels systèmes pour l’exploitation des ces méchanismes sous-jacent de façon portable. Mais globalement le concept est strictemet identique (modulo une petite simplification de la gestion de la stack).
La création de thread ce fait via :
pthread_t: type représentant un threadpthread_create(): pour créer et démarrer un thread (doc), les paramètres sont les suivants :- adresse où stocker l’identifiant du thread créé
- attributs par défaut (priorité, taille de pile, etc.) (souvent
NULL) - fonction à exécuter de type
void* (*)(void*) - argument passé à la fonction (notre structure) converti en
void*
pthread_join(): pour attendre la fin d’un thread (doc)- variable d’identifiant du thread a attendre
- adresse où stocker le pointeur de retour (type
void**)
- Un pointeur de fonction de type
void* (*)(void*): le “programme” du thread
Le concept pour parralléliser ce programme est le suivant :
Structure de données pour passer les paramètres :
En C, pthread_create() ne peut passer qu’un seul pointeur void* à la fonction thread. Pour passer plusieurs paramètres, on utilise une structure. Vous pouvez décalarer un tableau qui utilise cette structure et passé l’adresse de l’élément i aux thread créé. Voici un exemple :
typedef struct {
int valeur;
int multiplicateur;
int index;
} args_t;
Fonction thread :
La fonction exécutée par le thread doit avoir la signature : void* fonction(void* arg)
void* calcul_thread(void* arg) {
// cette fonction est appelé via pthread_create
// vous savez ce que représente @arg
// vous notifié au compilateur "cette" address dois être considéré comme args_t
args_t* args = (args_t*)arg;
// vous pouvez accéder (read/write) a toutes les variables via `args->`
return NULL;
}
Pourquoi void* partout ? Le type void* est un pointeur générique en C. C’est le mécanisme pour avoir du “polymorphisme” : la fonction peut accepter n’importe quel type de pointeur. À l’intérieur, on fait un cast vers le vrai type. C’est au programmeur de vérifier et de savoir quelle données correspond a ce pointeur !!! Important : Le thread doit pouvoir accéder aux données pendant toute sa durée de vie. Ne passez JAMAIS de pointeur vers une variable locale de la pile qui va disparaître !
Version parallèle
copier calcule_sequentiel.c en calcul_parallele.c
Maintenant vous allez orchestrer plusieurs threads pour diviser le travail. Le principe : au lieu de traiter les 10 éléments en séquence, créer 10 threads qui traitent chacun un élément en parallèle.
Étape 1 : Incluez les headers nécessaires
#include <pthread.h>
Étape 2 : Déclarez la structure et la fonction d’éxécution des threads
Étape 3 : Parallélisé le main() parallèle
int main(void) {
printf("=== Calcul Parallèle ===\n");
struct timespec debut, fin;
clock_gettime(CLOCK_MONOTONIC, &debut);
// TODO: Créer un tableau de pthread_t
// TODO: Créer un tableau de structures d'arguments
// TODO: Créer et démarrer tous les threads
// TODO : Créer tous les threads
// TODO: Attendre tous les threads
// TODO: Agréger les résultats
clock_gettime(CLOCK_MONOTONIC, &fin);
long duree = (fin.tv_sec - debut.tv_sec) * 1000 + (fin.tv_nsec - debut.tv_nsec) / 1000000;
printf("Résultat total : %ld\n", somme_total);
printf("Durée : %ld ms\n", duree);
return 0;
}
Questions d’analyse
Après implémentation, répondez à ces questions :
- Performance : Quelle version est la plus rapide ? Pourquoi ?
- Mesurez : Différentes valeurs pour
MULTIPLICATEUR, jusqu’a à 100000000 et observez la différence - Résultats : Les résultats sont-ils identiques entre les versions ? Pourquoi n’y a-t-il pas de race condition ?
- Scalabilité : Testez avec différents nombres de threads. Que se passe-t-il si vous créez 100 threads pour 10 valeurs ?
Méthodologie : En principe ce style de benchmark dois être effectué plusieurs fois en prennant quelques statistiques (médiane, moyenne, std). Tous les couples
thread_count / multiplicateurdevraient être testé pour vérifier l’influence de chaque facteurs. Puis affiché avec un plot ou une heatmap afin de choisir le meilleur compromis pour un matériel cible (nombre de coeur). Il existe toute fois des rêgles basic, on consière globalement que le nombre de thread optimal est entre 2 et 8 threads par coeur physique. Le facteur change en fonction de la vitesse d’éxécution de la fonction à éxécuter ou encore du nombre d’I/O effectué (qui mettent le thread en pause).
Exemple d’un calcule sur un système comportant deux coeurs.
Race conditions
Ici le but est de démontrer que les écritures concurrentes sur des variables partagées créent des comportements imprévisibles appelés race conditions. Contrairement à l’exercice précédent où chaque thread avait ses propres données, ici tous les threads modifient la même variable. C’est le scénario classique qui génère des bugs de concurrence.
Créez compteur_dangereux.c : C’est le code de base problématique
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define INCREMENT 11111111
#define THREAD_COUNT 99
static long long compteur_global = 0; // Variable partagée MODIFIABLE et MODIFIÉ
void* incrementeur_thread(void* arg) {
for (int i = 0; i < INCREMENT; i++) {
// TODO: Expliquer pourquoi, dans le détail, cette ligne qui pose problème
compteur_global++; // RACE CONDITION ICI !
}
return NULL;
}
int main(void) {
printf("=== Compteur Dangereux ===\n");
long long expected = INCREMENT*THREAD_COUNT;
pthread_t threads[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++)
pthread_create(&threads[i], NULL, incrementeur_thread, NULL);
for (int i = 0; i < THREAD_COUNT; i++)
pthread_join(threads[i], NULL);
printf("Valeur attendue : %ld\n", expected);
printf("Résultat final : %ld\n", compteur_global);
printf("Différence : %ld\n", expected - compteur_global);
return 0;
}
Le but est de tester ce code plusieurs fois, qui devrait révéler le non-déterminisme des race conditions. Plus vous créez de contention (threads nombreux + opérations nombreuses), plus vous avez de coeurs utilisé (peut-être pas sur les vm), plus les erreurs apparaissent.
Étape 1 : Créez une fonction pour réinitialiser et tester. Il faudra également passé en allocation dynamique et utiliser un structure pour passé les arguments aux threads.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static int compteur_global = 0;
void reset_compteur(void) {
compteur_global = 0;
}
int get_compteur(void) {
return compteur_global;
}
// ... (même fonction incrementeur_thread que compteur_dangereux.c)
int executer_test(int nb_threads, int nb_increments) {
reset_compteur();
pthread_t* threads = malloc(nb_threads * sizeof(pthread_t));
// Instancier un tableau de structure pour les arguments.
// Création des threads
for (int i = 0; i < nb_threads; i++)
pthread_create(&threads[i], NULL, incrementeur_thread, NULL);
for (int i = 0; i < nb_threads; i++)
pthread_join(threads[i], NULL);
int resultat = get_compteur();
// Qu'est ce qui a été oublié ici ?
return resultat;
}
Étape 2 : Boucle de test avec statistiques
int main(void) {
const int NB_TESTS = 50;
const int NB_THREADS = 9;
const int NB_INCREMENTS = 1111111;
const int ATTENDU = NB_THREADS * NB_INCREMENTS;
printf("=== Test Race Condition ===\n");
printf("Configuration : %d threads × %d incréments\n", NB_THREADS, NB_INCREMENTS);
printf("Valeur attendue : %d\n\n", ATTENDU);
int nb_corrects = 0;
int min = ATTENDU;
int max = 0;
for (int test = 0; test < NB_TESTS; test++) {
int resultat = executer_test(NB_THREADS, NB_INCREMENTS);
if (resultat == ATTENDU) {
nb_corrects++;
printf("Test %2d : %d v \n", test + 1, resultat);
} else {
printf("Test %2d : %d x (différence: %d)\n", test + 1, resultat, ATTENDU - resultat);
}
if (resultat < min) min = resultat;
if (resultat > max) max = resultat;
}
printf("\n=== Statistiques ===\n");
printf("Tests corrects : %d/%d (%.1f%%)\n",
nb_corrects, NB_TESTS, (100.0 * nb_corrects) / NB_TESTS);
printf("Valeur min : %d (perte de %d)\n", min, ATTENDU - min);
printf("Valeur max : %d\n", max);
return 0;
}
Observation typique : Vous devriez voir des résultats variables, souvent inférieurs à 9999999. Plus il y a de threads, plus les “pertes” sont fréquentes.
Expérimentations suggérées :
- Changez le nombre de threads (1, 2, 5, 10, 20, 40, 60, 100)
- Impact sur la fréquence des race conditions ?
- Avec 1 thread, devrait toujours être correct - Changez le nombre d’incréments par thread
- Plus d’opérations = plus de problèmes ? - Compilez avec optimisations :
gcc -O2 -pthread test_compteur.c
- Les optimisations peuvent aggraver ou masquer le problème !
Protection avec Mutex
Un mutex (mutual exclusion) garantit qu’un seul thread peut exécuter une section critique à la fois. En C avec pthreads, on utilise pthread_mutex_t. Avant d’entrer dans une section critique, un thread doit acquérir le verrou avec pthread_mutex_lock(). Si ce verrou est déjà détenu par un autre thread, le thread courant est bloqué jusqu’à ce que le verrou soit libéré. Lorsque le thread propriétaire quitte la section critique, il libère le verrou avec pthread_mutex_unlock(), autorisant ainsi un autre thread en attente à acquérir le mutex.
TODO : Créez compteur_securise.c :
Étape 1 : Reprenez le code de compteur_dangereux.c comme base
Étape 2 : Ajoutez un mutex pour protéger le compteur. Il y a deux façons d’initialiser un mutex :
- Statique (pour variables globales) :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- Dynamique (pour malloc ou attributs personnalisés) :
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// ... utilisation ...
pthread_mutex_destroy(&mutex);
Étape 3 : Protégez les sections critiques
Même les lectures doivent être protégées ! Sur certaines architectures (32-bit lisant un int64_t, par exemple), une lecture peut ne pas être atomique. Le thread pourrait lire une valeur “à moitié écrite” par un autre thread. Règle d’or : Si une variable est écrite par plusieurs threads, toute lecture ET écriture doit être protégée par le même mutex. Au risque de lire des données inconsistantes.
Étape 4 : Créez test_mutex.c en reprenant exactement la même logique que test_compteur.c
Observation attendue : Vous devriez maintenant toujours obtenir le résultat attendu (9999999). Plus de variabilité, plus de “pertes”. La version synchronisée sera plus lente car les threads doivent attendre leur tour au lieu de travailler en parallèle. C’est le compromis sécurité/performance.
Bonnus proposé un benchmark avec une solution reposant sur fork
Read-Write Lock (pthread_rwlock_t)
Les mutex sont trop restrictifs pour certains cas d’usage. Imaginez un cache de données :
- Lectures : Très fréquentes (90% des accès), peuvent se faire en parallèle sans danger
- Écritures : Rares (10% des accès), nécessitent exclusivité
Avec un mutex classique, même les lectures se bloquent mutuellement alors qu’elles pourraient être parallèles !
Solution : Les read-write locks (verrous lecteur-écrivain) permettent :
- Plusieurs lecteurs simultanés : Tant qu’aucun écrivain n’est actif
- Un seul écrivain exclusif : Bloque tous les lecteurs et autres écrivains
- Des méchanismes internes permettent d’assurer que tous les lectures et écritures sont effectué.
Pour la doc -> pthread_rw_lockl
TODO : Créez cache_rwlock.c pour simuler un cache avec accès lecture/écriture
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#define TAILLE_CACHE 100
static int cache[TAILLE_CACHE];
// Initialiser le cache
void init_cache(void) {
for (int i = 0; i < TAILLE_CACHE; i++) {
cache[i] = i * 10;
}
}
// Lecture du cache (plusieurs lecteurs possibles en parallèle)
int lire_cache(int index) {
if (index < 0 || index >= TAILLE_CACHE) return -1;
// Protégez cett esection
int valeur = cache[index];
usleep(100);
return valeur;
}
// Écriture du cache (exclusif, bloque tout)
void ecrire_cache(int index, int valeur) {
if (index < 0 || index >= TAILLE_CACHE) return;
// Protégez cett esection
cache[index] = valeur;
usleep(500); // 0.5 ms
}
// Thread lecteur (90% du traffic)
void* thread_lecteur(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 100; i++) {
int index = rand() % TAILLE_CACHE;
int valeur = lire_cache(index);
if (i % 20 == 0) { // Affichage occasionnel
printf("[Lecteur %d] cache[%d] = %d\n", id, index, valeur);
}
}
printf("[Lecteur %d] terminé\n", id);
return NULL;
}
// Thread écrivain (10% du traffic)
void* thread_ecrivain(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 10; i++) {
int index = rand() % TAILLE_CACHE;
int valeur = rand() % 1000;
ecrire_cache(index, valeur);
printf("[Écrivain %d] cache[%d] = %d (mise à jour)\n", id, index, valeur);
usleep(1000); // Les écritures sont moins fréquentes
}
printf("[Écrivain %d] terminé\n", id);
return NULL;
}
int main(void) {
srand(time(NULL));
init_cache();
printf("=== Test Read-Write Lock ===\n\n");
struct timespec debut, fin;
clock_gettime(CLOCK_MONOTONIC, &debut);
// Créer 8 lecteurs et 2 écrivains
pthread_t lecteurs[8];
pthread_t ecrivains[2];
int ids_lecteurs[8];
int ids_ecrivains[2];
// Démarrer les lecteurs
for (int i = 0; i < 8; i++) {
ids_lecteurs[i] = i + 1;
pthread_create(&lecteurs[i], NULL, thread_lecteur, &ids_lecteurs[i]);
}
// Démarrer les écrivains
for (int i = 0; i < 2; i++) {
ids_ecrivains[i] = i + 1;
pthread_create(&ecrivains[i], NULL, thread_ecrivain, &ids_ecrivains[i]);
}
// Attendre tout le monde
for (int i = 0; i < 8; i++) {
pthread_join(lecteurs[i], NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(ecrivains[i], NULL);
}
clock_gettime(CLOCK_MONOTONIC, &fin);
long duree = (fin.tv_sec - debut.tv_sec) * 1000 + (fin.tv_nsec - debut.tv_nsec) / 1000000;
printf("\nDurée totale : %ld ms\n", duree);
pthread_rwlock_destroy(&rwlock_cache);
return 0;
}
Créez test_rwlock.c et comparer mutex vs rwlock :
// TODO: Créer deux versions du test ci-dessus
// 1. Avec pthread_mutex_t (toutes les lectures se bloquent)
// 2. Avec pthread_rwlock_t (lectures parallèles)
// Vous pouvez utiliser la compilation conditionel
// Comparer les performances
MPMC
Les sémaphores généralisent les mutex : au lieu de permettre 1 seul accès simultané, ils permettent N accès simultanés. Cette exercice est un pattern classique de la programmation concurrente, réutilisable dans de nombreux contextes (pools de ressources, pipelines de traitement, impression, etc.).
Les sémaphore sont une sorte de système de jetons :
- Le sémaphore possède un certain nombre de jetons initiale.
- Chaque thread qui veut accéder à la ressource mange un jeton (sem_wait).
- Quand il a fini, il rend le jeton (sem_post).
sem_wait(&sem) : prendre un jeton voir doc
- Si le
compteur > 0: décrémente le compteur, le thread continue immédiatement - Si le
compteur = 0: aucun jeton disponible, le thread est mis en attente (bloqué), il se réveillera lorsqu’un sem_post sera effectué par un autre thread sem_waitest atomique : aucune interférence possible entre la vérification et la décrémentation.
sem_post(&sem) : rendre/produit un jeton voir doc
- Incrémente le compteur
- Si des threads sont bloqués en attente, réveille un thread en attente
sem_postne bloque jamais.
Il existent d’autres fonction intéréssantes telque sem_trywait qui n’est pas bloquant (retour 0 ou 1 si le jeton est prit) et sem_timedwait qui bloque pendent un quantum de temps.
Attention : sémaphore binaire ≠ mutex Bien qu’un sémaphore initialisé à 1 se comporte comme un mutex, c’est une mauvaise pratique de l’utiliser ainsi : Pas d’optimisations mutex (priority inheritance, etc.) ! Règle : Utilisez un mutex pour l’exclusion mutuelle, un sémaphore pour compter des ressources.
Contexte : Un buffer de taille limitée partagé entre :
- Producteurs : Ajoutent des éléments (bloqués si buffer plein)
- Consommateurs : Retirent des éléments (bloqués si buffer vide)
Défis de synchronisation :
- Éviter que les producteurs remplissent un buffer plein : contrôle de flux
- Éviter que les consommateurs vident un buffer vide : synchronisation production / consommation
- Protéger l’accès concurrent au buffer : exclusion mutuelle sur la structure de données
Architecture de la solution : Nous utiliserons des sémaphores pour résoudre ces problèmes indépendamment.
TODO : Créez producteur_consommateur.c:
Étape 1 : Déclarez les sémaphores : Les sémaphores s’équilibrent tel que places_libres + elements_disponibles = TAILLE_BUFFER (toujours vrai !)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define TAILLE_BUFFER 5
// Buffer circulaire
static int buffer[TAILLE_BUFFER];
static int index_prod = 0;
static int index_cons = 0;
// TODO: Déclarer 3 sémaphores avec les bonnes valeurs initiales viq sem_init
// Le deuxième paramètre `0` signifie "partagé entre threads du même processus" (vs `1` pour inter-processus).
static sem_t places_libres;
static sem_t elements_disponibles;
static sem_t mutex_buffer; // Qulle est le soucis ici ?
Étape 2 : Implémentez les fonctions de manipulation du buffer
void ajouter_buffer(int produit) {
// Quelle soucis peut on imaginer ici ?
buffer[index_prod] = produit;
index_prod = (index_prod + 1) % TAILLE_BUFFER; // Circulaire
}
int retirer_buffer(void) {
// Quelle soucis peut on imaginer ici ?
int produit = buffer[index_cons];
index_cons = (index_cons + 1) % TAILLE_BUFFER; // Circulaire
return produit;
}
Étape 3 : Implémentez le thread producteur
void* thread_producteur(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 10; i++) {
int produit = id * 100 + i; // Produit unique
// TODO: 1. Attendre une place libre
// TODO: 2. Ajouter l'élément au buffer
ajouter_buffer(produit);
printf("[Producteur %d] Ajouté : %d (index=%d)\n",
id, produit, (index_prod - 1 + TAILLE_BUFFER) % TAILLE_BUFFER);
// TODO: 3. Signaler qu'un élément est disponible
usleep(50);
}
printf("[Producteur %d] terminé (10 produits)\n", id);
return NULL;
}
Ordre crucial des sem_wait() ! Si le buffer est plein et qu’on bloque sur places_libres après avoir pris le mutex, les consommateurs ne peuvent pas accéder au buffer pour libérer des places → deadlock ! Règle : Toujours sem_wait() sur les sémaphores de ressources avant les mutex.
Étape 4 : Implémentez le thread consommateur
void* thread_consommateur(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 7; i++) {
// TODO: 1. Attendre qu'un élément soit disponible
// TODO: 2. Retirer un élément du buffer
int produit = retirer_buffer();
printf("[Consommateur %d] Retiré : %d (index=%d)\n",
id, produit, (index_cons - 1 + TAILLE_BUFFER) % TAILLE_BUFFER);
// TODO: 3. Signaler qu'une place est libre
sem_post(&places_libres);
usleep(50);
}
printf("[Consommateur %d] terminé (7 produits)\n", id);
return NULL;
}
Symétrie : Le consommateur fait exactement l’inverse du producteur. Chaque sem_wait() du producteur correspond à un sem_post() du consommateur et vice-versa.
Étape 5 : Implémentez le main avec tests
int main(void) {
srand(time(NULL));
// Initialisation des sémaphores
printf("=== Producteur-Consommateur ===\n");
printf("Taille buffer : %d\n\n", TAILLE_BUFFER);
// Scénario : 2 producteurs × 10 = 20 produits
// 3 consommateurs × 7 = 21 demandes
// → 1 consommateur va bloquer indéfiniment !
pthread_t producteurs[2];
pthread_t consommateurs[3];
int ids_prod[2] = {1, 2};
int ids_cons[3] = {1, 2, 3};
// Démarrage
for (int i = 0; i < 2; i++)
pthread_create(&producteurs[i], NULL, thread_producteur, &ids_prod[i]);
for (int i = 0; i < 3; i++)
pthread_create(&consommateurs[i], NULL, thread_consommateur, &ids_cons[i]);
// Attente des producteurs
for (int i = 0; i < 2; i++)
pthread_join(producteurs[i], NULL);
printf("\n Tous les producteurs terminés !\n");
printf(" Un consommateur va bloquer car 3×7=21 > 2×10=20\n\n");
// Attendre 2 secondes puis annuler les consommateurs bloqués
sleep(2);
for (int i = 0; i < 3; i++) {
pthread_cancel(consommateurs[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(consommateurs[i], NULL);
}
// Nettoyage
sem_destroy(&places_libres);
sem_destroy(&elements_disponibles);
sem_destroy(&mutex_buffer);
return 0;
}
TODO : Créez test_semaphore.c avec différents scénarios :
// Scénario 1 : Équilibré (production = consommation)
// 3 producteurs × 10 = 30 produits
// 5 consommateurs × 6 = 30 consommations
// Scénario 2 : Producteurs plus rapides
// 5 producteurs × 10 = 50 produits
// 2 consommateurs × 10 = 20 consommations
// Scénario 3 : Consommateurs plus rapides
// 1 producteur × 50 = 50 produits
// 10 consommateurs × 10 = 100 consommations
// TODO: Implémenter ces 3 scénarios et observer les comportements
