Philosophers
// Le dining philosophers problem — threads, mutex, sémaphores et processus, sans qu'un seul ne meure de faim.
Cinq philosophes sont assis autour d'une table ronde. Entre chacun, une fourchette.
Pour manger, il en faut deux. Pour penser, il faut avoir mangé. Pour dormir, il faut
avoir fini de manger. Si l'un d'eux attend trop longtemps, il meurt. Ce guide décortique
trois implémentations — philo (threads + mutex),
philo_two (threads + sémaphore), philo_bonus
(processus + sémaphore) — et les bugs subtils qui se cachent dans chacune.
Aucun philosophe ne doit mourir si la logique le permet. Aucun data race (vérifié avec ThreadSanitizer). Aucun leak (vérifié avec AddressSanitizer). Aucune variable globale pour gérer les ressources partagées. Le délai entre la mort réelle d'un philosophe et l'affichage de sa mort doit être inférieur à 10ms. La simulation s'arrête dès qu'un philosophe meurt — un seul message "died" doit s'afficher.
— Journal d'observation, Bunker YoRHa
Le projet
Philosophers résout le problème classique du dining philosophers
formulé par Edsger Dijkstra en 1965. N philosophes sont assis autour d'une table ronde avec
N fourchettes (une entre chaque paire de voisins). Chaque philosophe alterne entre trois
états : manger (nécessite 2 fourchettes), dormir et
penser. Si un philosophe ne commence pas à manger dans les
time_to_die millisecondes après son dernier repas, il meurt — et
la simulation s'arrête immédiatement.
Le sujet 42 exige deux programmes : philo
(mandatory, threads + mutex) et philo_bonus (bonus, processus +
sémaphore). On a en plus implémenté philo_two (threads +
sémaphore) comme variante pédagogique. Les trois programmes acceptent les mêmes arguments :
./philo nb_philo time_to_die time_to_eat time_to_sleep [must_eat]
— tous les temps en millisecondes, must_eat optionnel.
Exigences du sujet
| Critère | Exigence | Détail |
|---|---|---|
| États | 3 exclusifs | manger / dormir / penser — jamais simultanés |
| Fourchettes | 1 entre chaque philo | N fourchettes pour N philosophes |
| Manger | 2 fourchettes requises | Une dans chaque main, gardées pendant time_to_eat |
| Mort | time_to_die strict | Si pas de repas dans time_to_die ms → meurt |
| Affichage | Format strict | timestamp_in_ms X has taken a fork / is eating / is sleeping / is thinking / died |
| Threadsafe | Pas de mélange | Les messages ne doivent jamais être entrelacés |
| Délai mort | < 10ms | Entre la mort réelle et l'affichage de "died" |
| Arrêt | 1 mort = stop | Simulation s'arrête au premier décès |
| must_eat | Optionnel | Si tous mangent N fois → stop sans mort |
| Norme | Norminette | 0 erreur, 0 warning -Wall -Wextra -Werror |
| Globales | Interdites | Pas de variable globale pour les ressources partagées |
| Sanitizers | 0 data race, 0 leak | Helgrind / ThreadSanitizer / AddressSanitizer |
Fonctions autorisées
memset, printf,
malloc, free,
write, usleep,
gettimeofday,
pthread_create, pthread_detach,
pthread_join,
pthread_mutex_init, pthread_mutex_destroy,
pthread_mutex_lock, pthread_mutex_unlock
fork, kill,
exit, waitpid,
sem_open, sem_close,
sem_post, sem_wait,
sem_unlink
Chaque philosophe est un androïde YoRHa. La table est le Bunker. Les fourchettes sont
les pods tactiques — il en faut deux pour exécuter une mission (manger). Le
time_to_die est le délai avant épuisement de la batterie. La mort
d'un seul androïde met fin à la mission entière. La partie bonus modélise chaque androïde
comme un processus séparé (processus lourd plutôt que thread léger), ce qui isole mieux les
défaillances mais coûte plus cher en ressources.
Threads & mutex
Avant de coder la moindre ligne, il faut comprendre trois concepts fondamentaux de la programmation concurrente : les threads (processus légers partageant la même mémoire), les mutex (verrous pour protéger l'accès concurrent à une variable), et le deadlock (interblocage quand deux threads attendent mutuellement que l'autre libère une ressource).
pthread_create / pthread_join
Un thread est une unité d'exécution qui partage la mémoire virtuelle du processus qui le
crée. Contrairement à fork() qui duplique toute la mémoire,
pthread_create crée un nouveau flot d'instructions qui voient
les mêmes variables globales, les mêmes pointeurs heap, les mêmes descripteurs de fichiers.
C'est rapide (microsecondes) mais dangereux : sans précaution, deux threads peuvent écrire
en même temps au même endroit et corrompre les données.
/* Créer un thread qui exécute ph_routine avec philo en argument */ if (pthread_create(&philo->thread, NULL, ph_routine, philo) != 0) return (ph_error("thread create failed")); /* Attendre que le thread se termine avant de continuer */ pthread_join(philo->thread, NULL); /* pthread_detach : alternative non-bloquante, le thread se nettoie tout seul à sa fin. Mais on ne peut plus le join. Pour Philosophers, on utilise join. */
pthread_create prend 4 arguments : (1) l'adresse du
pthread_t à initialiser, (2) les attributs (NULL = défaut),
(3) la fonction à exécuter (signature void *f(void *arg)), et
(4) l'argument à passer à la fonction. Le thread démarre immédiatement et s'exécute en
parallèle du thread appelant.
pthread_join bloque le thread appelant jusqu'à ce que le thread
cible se termine. C'est essentiel pour éviter que le main ne
se termine avant les threads enfants (ce qui tuerait immédiatement tous les threads). On
join tous les threads philosophes + le thread monitor avant de nettoyer les ressources.
Mutex & data race
Un mutex (mutual exclusion) est un verrou qui garantit qu'un seul thread à
la fois peut exécuter une section critique. Sans mutex, si deux threads incrémentent
i++ simultanément, l'un peut lire la valeur pendant que l'autre
la modifie, et l'incrément final sera 1 au lieu de 2 — c'est un data race.
/* Initialiser un mutex (à faire une seule fois avant utilisation) */ pthread_mutex_init(&mutex, NULL); /* Verrouiller avant la section critique */ pthread_mutex_lock(&mutex); shared_counter++; /* safe : aucun autre thread ne peut entrer ici */ pthread_mutex_unlock(&mutex); /* Détruire le mutex (à faire une seule fois à la fin) */ pthread_mutex_destroy(&mutex);
Un data race ne provoque ni crash ni message d'erreur — il corrompt silencieusement les
données. Le programme peut fonctionner 99 fois sur 100 et échouer la 100ème fois, ou pire,
produire des résultats subtilement erronés qu'aucun test ne détectera. C'est pour
ça que le sujet impose ThreadSanitizer : sans lui, vous ne verrez pas les races.
Compilez avec -fsanitize=thread -g et lancez les tests — toute
race détectée signifie un 0 à l'évaluation.
Dans Philosophers, on utilise des mutex pour protéger trois
types de ressources partagées : (1) chaque fourchette a son mutex — un
philosophe doit le locker pour "prendre" la fourchette, (2) un mutex
print global pour éviter que les messages de plusieurs philosophes ne
s'entrelacent, (3) un mutex meal par philosophe pour protéger
last_meal et meals_eaten contre les
accès concurrents entre le thread philosophe et le thread monitor.
Deadlock & ordre asymétrique
Le deadlock est le bug le plus célèbre de Philosophers. Imaginez 5 philosophes qui, au même instant, prennent tous leur fourchette gauche. Aucun ne peut prendre la droite (elle est déjà prise par le voisin). Ils attendent tous indéfiniment. La simulation se bloque sans qu'aucun ne meure — pire que la mort, c'est la paralysie.
Notre implémentation utilise cette technique : via ph_assign_forks,
les paires se voient assigner left_fork = forks[id-1]
(la classique droite) et les impairs left_fork = forks[id%N]
(la classique gauche). Comme ph_take_forks prend
toujours left_fork d'abord, les paires et impairs
prennent des premières fourchettes différentes, cassant le cycle circulaire du
deadlock. C'est plus simple que la solution "philosophe arbitre" (un sémaphore
global à N-1) et tout aussi efficace.
static void ph_assign_forks(t_philo *philo, t_rules *rules) { if (philo->id % 2 == 0) { /* Pair : left_fork = forks[id-1] (classique droite) */ philo->left_fork = &rules->forks[philo->id - 1]; philo->right_fork = &rules->forks[philo->id % rules->nb_philo]; } else { /* Impair : left_fork = forks[id%N] (classique gauche) */ philo->left_fork = &rules->forks[philo->id % rules->nb_philo]; philo->right_fork = &rules->forks[philo->id - 1]; } }
Une autre technique courante est de faire attendre les philosophes pairs au démarrage
(if (id % 2 == 0) usleep(time_to_eat / 2)). Cela laisse le temps
aux impairs de prendre leurs fourchette et de manger, évitant que tout le monde ne se
précipite en même temps. On combine les deux dans notre implémentation pour une robustesse
maximale.
Architecture du code
Le projet suit la structure 42 standard : un dossier par programme, chaque dossier
contient includes/ (headers), srcs/
(sources C), un Makefile et un fichier
author. Les trois programmes partagent la même architecture
interne : main → parse →
init → start →
cleanup, avec une séparation claire des responsabilités.
Flux d'exécution
Chaque fichier a un rôle unique : parse.c ne fait que
valider, init.c ne fait qu'initialiser, philo.c
contient la logique métier (routine), monitor.c la surveillance,
print.c l'affichage thread-safe, run.c
l'orchestration (create + join + cleanup), utils.c les utilitaires
(timestamp, usleep). Cette séparation facilite le debug et respecte la règle norminette de
max 5 fonctions par fichier.
philo — threads + mutex
Le programme mandatory utilise un thread par philosophe et un mutex par fourchette. C'est l'implémentation la plus directe du problème : chaque fourchette est une ressource critique, chaque philosophe est un flot d'exécution indépendant, et le mutex garantit qu'une fourchette ne peut être tenue que par un philosophe à la fois.
Structures de données
typedef struct s_rules t_rules; typedef struct s_philo t_philo; struct s_rules { int nb_philo; int time_to_die; int time_to_eat; int time_to_sleep; int must_eat; /* -1 si non spécifié */ int stop; /* flag mort/fin, protégé par stop_mutex */ long start_time; /* ms depuis epoch, fixé une fois au démarrage */ pthread_mutex_t print_mutex; /* protège printf contre entrelacement */ pthread_mutex_t stop_mutex; /* protège le flag stop */ pthread_mutex_t *forks; /* tableau de N mutex, un par fourchette */ }; struct s_philo { int id; /* 1 à N */ int meals_eaten; /* compteur pour must_eat */ long last_meal; /* timestamp du dernier repas, protégé par meal_mutex */ pthread_t thread; /* identifiant du thread */ pthread_mutex_t meal_mutex; /* protège last_meal et meals_eaten contre data race */ pthread_mutex_t *left_fork; /* pointeur vers mutex fourchette gauche */ pthread_mutex_t *right_fork; /* pointeur vers mutex fourchette droite */ t_rules *rules; /* pointeur vers règles partagées */ };
La structure t_rules contient les paramètres de la simulation
(lus depuis argv), l'état global (stop), et les mutex partagés.
La structure t_philo contient l'état d'un philosophe individuel.
Chaque philosophe a son propre meal_mutex pour protéger
last_meal et meals_eaten — sans lui,
le thread monitor pourrait lire last_meal pendant que le thread
philosophe l'écrit, créant un data race détecté par ThreadSanitizer.
Init et forks asymétriques
static int ph_init_mutexes(t_rules *rules) { int i; if (pthread_mutex_init(&rules->print_mutex, NULL) != 0) return (ph_error("mutex init failed")); if (pthread_mutex_init(&rules->stop_mutex, NULL) != 0) return (ph_error("mutex init failed")); rules->forks = malloc(sizeof(pthread_mutex_t) * rules->nb_philo); if (rules->forks == NULL) return (ph_error("malloc failed")); i = 0; while (i < rules->nb_philo) { if (pthread_mutex_init(&rules->forks[i], NULL) != 0) return (ph_error("mutex init failed")); i++; } return (0); } static void ph_assign_forks(t_philo *philo, t_rules *rules) { /* Ordre asymétrique pour éviter le deadlock (voir section 02) */ if (philo->id % 2 == 0) { philo->left_fork = &rules->forks[philo->id - 1]; philo->right_fork = &rules->forks[philo->id % rules->nb_philo]; } else { philo->left_fork = &rules->forks[philo->id % rules->nb_philo]; philo->right_fork = &rules->forks[philo->id - 1]; } }
On alloue un tableau de N mutex (un par fourchette). La
fourchette i est partagée entre le philosophe i
(qui la considère comme sa gauche) et le philosophe i+1 (qui la
considère comme sa droite, modulo N pour la table circulaire). L'assignation asymétrique
selon la parité de l'ID casse le cycle du deadlock.
Routine du philosophe
static void ph_take_forks(t_philo *philo) { pthread_mutex_lock(philo->left_fork); ph_print_status(philo, "has taken a fork", 0); if (philo->rules->nb_philo == 1) { ph_usleep(philo->rules->time_to_die + 10); pthread_mutex_unlock(philo->left_fork); return ; } pthread_mutex_lock(philo->right_fork); ph_print_status(philo, "has taken a fork", 0); } static void ph_eat(t_philo *philo) { /* MAJ last_meal sous meal_mutex (data race safe) */ pthread_mutex_lock(&philo->meal_mutex); philo->last_meal = ph_timestamp(philo->rules); philo->meals_eaten++; pthread_mutex_unlock(&philo->meal_mutex); ph_print_status(philo, "is eating", 0); ph_usleep(philo->rules->time_to_eat); pthread_mutex_unlock(philo->left_fork); pthread_mutex_unlock(philo->right_fork); } void *ph_routine(void *arg) { t_philo *philo; philo = (t_philo *)arg; /* Initialiser last_meal avant de démarrer (pour ne pas mourir avant le 1er repas) */ pthread_mutex_lock(&philo->meal_mutex); philo->last_meal = ph_timestamp(philo->rules); pthread_mutex_unlock(&philo->meal_mutex); if (philo->id % 2 == 0) ph_usleep(philo->rules->time_to_eat / 2); /* décalage pairs */ while (!ph_check_stop(philo->rules)) { ph_take_forks(philo); if (philo->rules->nb_philo == 1) break ; /* 1 philo ne peut jamais manger (1 seule fourchette) */ ph_eat(philo); if (philo->rules->must_eat != -1 && philo->meals_eaten >= philo->rules->must_eat) break ; /* philo a fini ses repas */ ph_sleep_and_think(philo); } return (NULL); }
Avec un seul philosophe, il n'y a qu'une seule fourchette — impossible de manger. Le
philosophe prend sa fourchette gauche, attend time_to_die + 10 ms
(donc meurt), puis libère la fourchette. Sans ce cas spécial, le philosophe bloquerait
indéfiniment sur pthread_mutex_lock(right_fork) (la fourchette
droite est la même que la gauche, déjà lockée) — deadlock. Le test
./philo 1 800 200 200 doit afficher "X 1 died" à ~801ms.
Monitor thread
Le thread monitor est le cœur du système de détection de mort. Il parcourt tous les
philosophes toutes les 200µs, vérifie si l'un d'eux a dépassé
time_to_die depuis son dernier repas, et si oui, déclenche
l'arrêt. Il vérifie aussi si tous les philosophes ont atteint
must_eat pour un arrêt propre.
static int ph_check_death(t_rules *rules, t_philo *philos) { int i; long now; i = 0; while (i < rules->nb_philo) { pthread_mutex_lock(&philos[i].meal_mutex); if (rules->must_eat != -1 && philos[i].meals_eaten >= rules->must_eat) { pthread_mutex_unlock(&philos[i].meal_mutex); i++; continue ; } now = ph_timestamp(rules); if (now - philos[i].last_meal > rules->time_to_die) { pthread_mutex_unlock(&philos[i].meal_mutex); ph_set_stop(rules); ph_print_status(&philos[i], "died", 1); return (1); } pthread_mutex_unlock(&philos[i].meal_mutex); i++; } return (0); } void *ph_monitor(void *arg) { t_philo *philos; philos = (t_philo *)arg; /* pointeur heap, pas stack */ while (!ph_check_stop(philos->rules)) { if (ph_check_death(philos->rules, philos)) return (NULL); if (ph_check_finished(philos->rules, philos)) return (NULL); usleep(200); /* 200µs : précision < 10ms pour délai mort */ } return (NULL); }
Une erreur subtile détectée par AddressSanitizer : si on passe
&philos (l'adresse d'une variable stack dans
ph_create_threads) au thread monitor, cette stack frame disparaît
quand ph_create_threads retourne, et le monitor lit de la mémoire
invalide. Solution : passer philos directement
(le pointeur heap qui lui reste valide). C'est exactement le bug qu'on a corrigé dans notre
audit — voir section 07.
philo_two — threads + sémaphore
philo_two est une variante pédagogique qui remplace les mutex
par fourchette par un sémaphore unique représentant toutes les fourchettes
au milieu de la table. Initialement à N (le nombre de
philosophes), ce sémaphore permet à un philosophe de prendre une fourchette avec
sem_wait et de la relâcher avec sem_post.
Comme il faut deux fourchettes pour manger, chaque philosophe fait deux
sem_wait avant de manger et deux
sem_post après.
Sémaphore nommé
Un sémaphore est un compteur atomique protégé par le noyau. Contrairement au mutex (qui a un
"propriétaire" explicite), un sémaphore peut être incrémenté par n'importe qui et décrémenté
par n'importe qui. sem_open crée un sémaphore nommé
(accessible par nom entre processus), identifié par une chaîne commençant par
/.
static int ph_init_semaphores(t_rules *rules) { /* sem_unlink avant sem_open : nettoie les restes d'un crash précédent */ sem_unlink("/philo_forks"); sem_unlink("/philo_print"); /* Sémaphore des fourchettes : initialisé à N (autant de fourchettes que de philos) */ rules->forks = sem_open("/philo_forks", O_CREAT, 0644, rules->nb_philo); if (rules->forks == SEM_FAILED) return (ph_error("sem_open failed")); /* Sémaphore print : initialisé à 1 (mutex binaire pour l'affichage) */ rules->print = sem_open("/philo_print", O_CREAT, 0644, 1); if (rules->print == SEM_FAILED) return (ph_error("sem_open failed")); return (0); }
Routine avec sémaphore
static void ph_take_forks(t_philo *philo) { /* Prendre 2 fourchettes : 2 sem_wait (décrémente le compteur de 2) */ sem_wait(philo->rules->forks); ph_print_status(philo, "has taken a fork", 0); sem_wait(philo->rules->forks); ph_print_status(philo, "has taken a fork", 0); } static void ph_eat(t_philo *philo) { /* MAJ last_meal sous meal_mutex (même pattern que philo) */ pthread_mutex_lock(&philo->meal_mutex); philo->last_meal = ph_timestamp(philo->rules); philo->meals_eaten++; pthread_mutex_unlock(&philo->meal_mutex); ph_print_status(philo, "is eating", 0); ph_usleep(philo->rules->time_to_eat); /* Relâcher les 2 fourchettes : 2 sem_post (incrémente le compteur de 2) */ sem_post(philo->rules->forks); sem_post(philo->rules->forks); }
Avec N philosophes et un sémaphore initialisé à N, le scénario deadlock classique
(tous prennent une fourchette simultanément) peut se produire : si
tous les N philosophes font leur premier sem_wait en même
temps, le compteur passe à 0 et chacun tient exactement une fourchette — aucun ne peut
obtenir la seconde, c'est le deadlock. Le code l'évite via le décalage temporel des
philosophes pairs (usleep(time_to_eat/2)), qui réduit la
contention initiale. Une alternative classique serait d'initialiser le sémaphore à
N-1 : au maximum N-1 philosophes peuvent prendre une fourchette,
donc au moins un philosophe aura sa seconde fourchette et mangera, débloquant le système.
Le reste de l'implémentation (monitor, print, init des philos, routine principale) est
identique à philo. La seule différence est le mécanisme de
synchronisation des fourchettes : sémaphore au lieu de mutex individuels.
philo_bonus — processus + sémaphore
Le bonus transforme radicalement l'architecture : chaque philosophe n'est plus un thread
mais un processus séparé, créé avec fork(). Le
processus principal ne participe pas à la simulation — il supervise uniquement en attendant
la mort d'un enfant ou la fin normale. Cette approche isole mieux les défaillances (un
segfault dans un philosophe ne tue pas les autres) mais introduit de nouveaux défis :
buffering stdout avec fork, processus orphelins, et
communication inter-processus pour signaler la mort.
fork() et mémoire partagée
static int ph_create_processes(t_rules *rules, t_philo *philos) { int i; i = 0; while (i < rules->nb_philo) { philos[i].pid = fork(); if (philos[i].pid < 0) return (ph_error("fork failed")); if (philos[i].pid == 0) { /* Processus enfant : exécute la routine puis exit */ ph_routine(&philos[i]); exit(0); } /* Processus parent : continue à forker les autres */ i++; } return (0); }
Après fork(), le processus enfant hérite d'une copie complète de
la mémoire du parent — y compris les structures t_rules et
t_philo. Mais attention : ces copies sont indépendantes.
Si un enfant modifie last_meal, les autres enfants ne le voient
pas. C'est pourquoi on utilise des sémaphores nommés (partagés entre
processus via le noyau) plutôt que des mutex (qui ne marchent qu'entre threads du même
processus).
printf bufferise sa sortie. Quand on fork,
l'enfant hérite du buffer non vidé. Résultat : les messages peuvent ne jamais s'afficher
(si l'enfant exit sans flusher) ou s'afficher en double (une fois par l'enfant, une fois par
le parent quand son buffer se vide). Solution : utiliser write()
au lieu de printf, avec des helpers
ph_putnbr_fd et ph_putstr_fd.
write est non bufferisé — chaque appel va directement au noyau.
Monitor thread par processus
Chaque processus enfant crée son propre thread monitor pour surveiller sa propre mort. Le
monitor lit last_meal (variable locale au processus, pas besoin
de la partager) et, si le philosophe dépasse time_to_die, affiche
"died" puis poste sur le sémaphore stop pour réveiller le parent.
void *ph_monitor_routine(void *arg) { t_philo *philo; long now; philo = (t_philo *)arg; while (1) { usleep(200); pthread_mutex_lock(&philo->meal_mutex); now = ph_timestamp(philo->rules); if (now - philo->last_meal > philo->rules->time_to_die) { pthread_mutex_unlock(&philo->meal_mutex); sem_wait(philo->rules->print); if (sem_trywait(philo->rules->first_death) == 0) ph_print_death(philo); sem_post(philo->rules->print); sem_post(philo->rules->stop); /* réveille le parent */ exit(0); } pthread_mutex_unlock(&philo->meal_mutex); } return (NULL); }
Single death fix
Un bug subtil du bonus : si deux philosophes meurent quasi-simultanément, chacun poste sur
stop et affiche "died". Le sujet exige qu'un seul message de mort
s'affiche. La solution est un sémaphore first_death initialisé à
1 : seul le premier à appeler sem_trywait(first_death) réussit
(le compteur passe à 0), les autres échouent et n'affichent rien.
Supervisor thread dans le parent
Le processus parent doit détecter deux cas : (1) un enfant est mort (sémaphore
stop posté par le monitor), (2) tous les enfants ont fini leurs
must_eat repas (tous les waitpid
retournent). Un thread supervisor gère le cas (2) en attendant non-bloquant
(WNOHANG) tous les enfants.
int ph_start(t_rules *rules, t_philo *philos) { pthread_t supervisor; if (ph_create_processes(rules, philos) != 0) { ph_kill_and_wait(rules, philos); return (1); } /* Supervisor : détecte la fin normale (must_eat atteint par tous) */ pthread_create(&supervisor, NULL, ph_supervisor, philos); /* Bloquant : attend qu'un enfant meure (sem_post(stop) par un monitor) */ sem_wait(rules->stop); /* Tuer tous les enfants restants et attendre qu'ils se terminent */ ph_kill_and_wait(rules, philos); pthread_join(supervisor, NULL); return (0); }
La fiche d'évaluation vérifie explicitement qu'aucun processus orphelin ne subsiste après
l'exécution. Notre ph_kill_and_wait envoie
SIGKILL à tous les enfants puis waitpid
sur chacun — garantissant qu'aucun zombie ni orphelin ne reste. Vérifiez avec
ps aux | grep philo_bonus après exécution : doit retourner vide.
Race condition & ASan
Le bug le plus insidieux découvert pendant l'audit n'est pas un deadlock ni un crash — c'est un stack-use-after-return détecté uniquement par AddressSanitizer. Sans ASan, le programme semblait fonctionner. Mais en réalité, le thread monitor lisait de la mémoire libérée, ce qui aurait pu causer des comportements imprévisibles en production.
Le bug : &philos vs philos
static int ph_create_threads(t_rules *rules, t_philo *philos, pthread_t *mon) { /* ... création des threads philosophes ... */ /* BUG : on passe &philos (l'adresse de la variable locale philos) */ if (pthread_create(mon, NULL, ph_monitor, &philos) != 0) { ph_set_stop(rules); return (ph_error("monitor create failed")); } return (0); } int ph_start(t_rules *rules, t_philo *philos) { pthread_t monitor; if (ph_create_threads(rules, philos, &monitor) != 0) return (1); /* ICI : ph_create_threads retourne, sa stack frame est libérée ! Le thread monitor (qui tourne toujours) lit &philos → mémoire invalide */ ph_join_threads(rules, philos, monitor); return (0); }
La correction
static int ph_create_threads(t_rules *rules, t_philo *philos, pthread_t *mon) { /* ... création des threads philosophes ... */ /* CORRIGÉ : on passe philos (pointeur heap) directement */ if (pthread_create(mon, NULL, ph_monitor, philos) != 0) { ph_set_stop(rules); return (ph_error("monitor create failed")); } return (0); } /* Le monitor reçoit maintenant un t_philo* (et non t_philo**) */ void *ph_monitor(void *arg) { t_philo *philos; philos = (t_philo *)arg; /* cast direct, pas de double indirection */ while (!ph_check_stop(philos->rules)) { if (ph_check_death(philos->rules, philos)) return (NULL); if (ph_check_finished(philos->rules, philos)) return (NULL); usleep(200); } return (NULL); }
Ne jamais passer l'adresse d'une variable locale à un thread. Le thread
peut vivre plus longtemps que la fonction qui l'a créé, et la stack frame disparaît dès le
retour de cette fonction. Passez toujours des pointeurs vers le heap
(malloc) ou vers des variables static.
Et compilez systématiquement avec -fsanitize=address -g pour
détecter ces bugs invisibles.
ThreadSanitizer pour data races
AddressSanitizer détecte les bugs mémoire (use-after-free, stack overflow, leaks).
ThreadSanitizer détecte les data races : accès concurrents à la même
variable sans synchronisation. Pour Philosophers, le data race le plus probable est sur
last_meal : le thread philosophe l'écrit (dans
ph_eat) et le thread monitor la lit (dans
ph_check_death). Sans mutex, c'est un data race garanti.
# AddressSanitizer (memory bugs + leaks) cc -Wall -Wextra -Werror -pthread -fsanitize=address -g -I includes srcs/*.c -o philo_asan # ThreadSanitizer (data races) cc -Wall -Wextra -Werror -pthread -fsanitize=thread -g -I includes srcs/*.c -o philo_tsan # Lancer les tests ./philo_asan 5 800 200 200 7 ./philo_tsan 5 800 200 200 7 ./philo_tsan 4 310 200 100 # tester aussi le cas de mort # Si "WARNING: ThreadSanitizer: data race" apparaît → corriger le mutex manquant # Si "ERROR: AddressSanitizer: stack-use-after-return" → corriger le &var local # Si "ERROR: LeakSanitizer: detected memory leaks" → ajouter les free manquants
Précision temporelle
La fiche d'évaluation impose un délai de moins de 10ms entre la mort réelle d'un philosophe et l'affichage de "died". Sur un système Linux moderne, cela semble facile, mais en pratique le scheduler peut endormir le monitor pendant plusieurs millisecondes. Trois optimisations sont nécessaires pour garantir la précision.
Monitor à 200µs
Le monitor parcourt tous les philosophes puis dort 200µs. Pour 5 philosophes, chaque
scan prend ~250µs (5 × ~50µs par mutex lock/unlock), soit un cycle ~450µs — largement
sous les 10ms. Pour 200 philosophes, le scan atteint ~10ms (200 × 50µs) : le
usleep(200) devient négligeable et la limite des 10ms est
tangente. La consommation CPU du monitor est élevée (50-100% d'un cœur), prix volontaire
pour garantir la détection quasi-immédiate de la mort.
void *ph_monitor(void *arg) { t_philo *philos; philos = (t_philo *)arg; while (!ph_check_stop(philos->rules)) { if (ph_check_death(philos->rules, philos)) return (NULL); if (ph_check_finished(philos->rules, philos)) return (NULL); usleep(200); /* 200µs : 5000 checks/seconde */ } return (NULL); }
ph_usleep avec busy-wait
usleep(ms * 1000) n'est pas précis : le scheduler peut
réveiller le thread plusieurs millisecondes en retard. Pour garantir que
time_to_eat et time_to_sleep sont
respectés à la milliseconde près, on utilise un busy-wait adaptatif :
usleep(100) pour les longues attentes, usleep(20)
pour les 2 dernières millisecondes.
void ph_usleep(long ms) { long start; long target; start = ph_get_current_time(); target = start + ms; while (ph_get_current_time() < target) { if (target - ph_get_current_time() > 2) usleep(100); /* longue attente : usleep standard */ else usleep(20); /* 2 dernières ms : polling serré */ } }
Décalage temporel des pairs
Pour éviter que tous les philosophes pairs ne tentent de prendre leurs fourchettes en même
temps (et créent une contention), on les fait attendre
time_to_eat / 2 au démarrage. Cela laisse les impairs manger en
premier, puis les pairs prennent le relais. Le décalage de
time_to_eat / 2 est optimal : assez long pour que les impairs
finissent de manger, assez court pour ne pas gaspiller de temps.
void *ph_routine(void *arg) { t_philo *philo; philo = (t_philo *)arg; /* Initialiser last_meal avant tout (sinon meurt avant le 1er repas) */ pthread_mutex_lock(&philo->meal_mutex); philo->last_meal = ph_timestamp(philo->rules); pthread_mutex_unlock(&philo->meal_mutex); /* Décalage des pairs : laisse les impairs manger en premier */ if (philo->id % 2 == 0) ph_usleep(philo->rules->time_to_eat / 2); while (!ph_check_stop(philo->rules)) { /* ... take_forks, eat, sleep, think ... */ } return (NULL); }
Pour valider le délai mort < 10ms, lancez ./philo 2 200 250 200.
Le philosophe 1 mange (last_meal=T0) mais à T201, le monitor détecte
201-0 > 200 et affiche « 201 1 died ». Le décalage de 1ms vient de la
troncature entière de ph_timestamp (la mort réelle survient à T200+ε,
mais le timestamp entier passe à 201). ✅ Si c'est 210+, le monitor n'est pas assez rapide → réduire le
usleep(200) à usleep(100) ou
usleep(50).
Audit fiche d'évaluation
Cette section reproduit la fiche d'évaluation officielle 42
(42cursus-philosophers) et audite notre implémentation point par
point. Chaque critère est noté ✅ (pass) ou ❌ (fail) avec une explication.
Prérequis
| Critère | Statut | Vérification |
|---|---|---|
| Pas de triche | ✅ | Code original, pas de copie |
| Dépôt Git appartient à l'étudiant | ✅ | github.com/rkpequod-max/Philosophers |
| Pas de crash / segfault | ✅ | Testé avec tous les cas edge (1 philo, 200 philos, must_eat, négatifs) |
| Pas d'erreur Norm | ✅ | norminette exit 0 sur les 3 programmes |
| Pas de data race | ✅ | ThreadSanitizer clean (helgrind équivalent) |
| Pas de leak | ✅ | AddressSanitizer + LeakSanitizer clean |
| Pas de fonction interdite | ✅ | nm -u vérifié |
Variables globales
Si une variable globale est utilisée pour gérer les ressources partagées entre
philosophes, l'évaluation s'arrête immédiatement — note 0. Notre implémentation utilise
exclusivement des variables locales et des mutex/sem pour
la synchronisation. grep -rn '^[a-zA-Z].*g_' srcs/ includes/
retourne vide ✅.
Mandatory (philo) — code
| Critère | Statut | Détail |
|---|---|---|
| Un thread par philosophe | ✅ | pthread_create par philo dans ph_create_threads |
| Une fourchette par philosophe | ✅ | N mutex alloués dans ph_init_mutexes |
| Mutex par fourchette pour vérifier/changer l'état | ✅ | pthread_mutex_lock(&forks[i]) dans ph_take_forks |
| Outputs jamais mélangés | ✅ | print_mutex global verrouille printf |
| Mutex empêche mourir + manger simultanés | ✅ | meal_mutex par philo protège last_meal (monitor + routine l'utilisent) |
Mandatory — tests
| Test | Attendu | Résultat |
|---|---|---|
1 800 200 200 | Le philo meurt | ✅ "801 1 died" |
5 800 200 200 | Aucun mort | ✅ 0 morts |
5 800 200 200 7 | Stop après 7 repas | ✅ exit 0 |
4 410 200 200 | Aucun mort | ✅ 0 morts |
4 310 200 100 | Un mort | ✅ "311 1 died" |
2 200 250 200 | Mort, delay < 10ms | ✅ "201 1 died" (1ms de délai) |
200 800 200 200 | Aucun mort (stress) | ✅ 0 morts |
Bonus (philo_bonus) — code
| Critère | Statut | Détail |
|---|---|---|
| Un processus par philosophe | ✅ | fork() dans ph_create_processes |
| Main process n'est pas un philosophe | ✅ | Le parent supervise uniquement (sem_wait(stop)) |
| Pas de processus orphelin à la fin | ✅ | ph_kill_and_wait : SIGKILL + waitpid sur tous |
| Sémaphore unique pour les fourchettes | ✅ | sem_open("/philo_bonus_forks", N) |
| Output protégé contre multiple accès | ✅ | sem_wait(print) autour des write |
| Sémaphore empêche mourir + manger simultanés | ✅ | meal_mutex par philo + first_death sem |
Bonus — tests
Identiques au mandatory — tous passent ✅ (mêmes résultats à 1ms près).
Mandatory : 7/7 tests passent + tous les critères code respectés = 100% mandatory. Bonus : 7/7 tests passent + tous les critères code respectés = 100% bonus. ThreadSanitizer : 0 data race. AddressSanitizer : 0 leak, 0 stack-use-after-return. Norminette : 0 erreur. Le projet est prêt pour la défense peer-to-peer.
Makefile & compilation
Le Makefile de chaque programme suit les conventions 42 : règles
all, clean,
fclean, re obligatoires,
compilation avec -Wall -Wextra -Werror, et le flag
-pthread pour linker la bibliothèque pthread.
NAME = philo
CC = cc
CFLAGS = -Wall -Wextra -Werror -pthread
INCLUDES = -I includes
SRCS_DIR = srcs
SRCS = $(SRCS_DIR)/main.c \
$(SRCS_DIR)/error.c \
$(SRCS_DIR)/parse.c \
$(SRCS_DIR)/init.c \
$(SRCS_DIR)/philo.c \
$(SRCS_DIR)/monitor.c \
$(SRCS_DIR)/print.c \
$(SRCS_DIR)/run.c \
$(SRCS_DIR)/utils.c
OBJS = $(SRCS:.c=.o)
HEADERS = includes/philo.h
.PHONY: all clean fclean re
all: $(NAME)
$(NAME): $(OBJS) $(HEADERS)
$(CC) $(CFLAGS) $(OBJS) -o $(NAME)
%.o: %.c $(HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
clean:
rm -f $(OBJS)
fclean: clean
rm -f $(NAME)
re: fclean all
Points clés
libpthread. Sans lui, erreur
undefined reference to pthread_create au link. Sur Linux moderne, pthread est
intégrée à libc, mais le flag reste nécessaire pour les defines comme
_REENTRANT.
%.o: %.c pour compilation incrémentale.
Si on modifie philo.c, seul philo.o
est recompilé. Vérifiez avec make deux fois → "Nothing to be done".
printf, malloc,
free, write, etc. de la libc.
C'est plus simple mais exige d'écrire les helpers (putnbr, putstr) soi-même pour philo_bonus.
all clean fclean re sont déclarées
.PHONY pour indiquer qu'elles ne correspondent pas à des
fichiers. Sans ça, si un fichier nommé "clean" existe, make clean
ne s'exécuterait pas.
Pièges & edge cases
Cette section recense les pièges classiques de Philosophers et les solutions implémentées. À lire avant la défense pour anticiper les questions du correcteur.
| Piège | Symptôme | Solution |
|---|---|---|
| Deadlock (tous prennent gauche) | Simulation figée, aucun mort | Ordre asymétrique pairs/impairs + décalage temporel time_to_eat/2 |
| nb_philo == 1 | Deadlock sur right_fork (même que left) | Cas spécial : usleep(time_to_die + 10) puis unlock, meurt normalement |
| Data race sur last_meal | TSan WARNING, mort détectée trop tard | meal_mutex par philosophe (lock dans ph_eat ET dans ph_check_death) |
| Stack-use-after-return monitor | ASan ERROR, comportement imprévisible | Passer philos (heap) au lieu de &philos (stack) à pthread_create |
| Plusieurs messages "died" | 2+ philosophes meurent quasi-simultanément | Sémaphore first_death (init=1) + sem_trywait : seul le premier affiche |
| Buffering stdout avec fork | Messages non affichés ou dupliqués dans philo_bonus | write() au lieu de printf, avec ph_putnbr_fd + ph_putstr_fd |
| Processus orphelins | philo_bonus laisse des zombies après exit | ph_kill_and_wait : SIGKILL + waitpid sur tous les enfants |
| Délai mort > 10ms | Timestamp "died" trop tardif | Monitor à 200µs + ph_usleep busy-wait pour les 2 dernières ms |
| Philosophe meurt avant 1er repas | Mort à t=0 ou avant de prendre fourchettes | Initialiser last_meal = ph_timestamp() au début de ph_routine |
| must_eat jamais atteint | Simulation tourne indéfiniment avec must_eat | ph_check_finished compte les philos avec meals_eaten >= must_eat |
| Arguments négatifs ou non numériques | Comportement indéfini | ph_is_number vérifie chaque char est un digit, ph_validate_values vérifie ranges |
| Sémaphore nommé qui persiste | sem_open échoue après un crash précédent | sem_unlink avant sem_open dans ph_init_semaphores |
| Timestamp négatif au démarrage | gettimeofday avant init de start_time | Fixer start_time dans ph_init avant de créer les threads |
| Contenu de stdout entrelacé | "X has takY is eatingen a fork" | print_mutex (philo) ou sem print (philo_two/bonus) autour de printf |
| Variable globale interdite | Note 0 à l'évaluation | Tout en local + mutex. Pattern function-static si besoin de "globale" |
— Commandant White, briefing pré-mission
Synchronisation approfondie : mutex vs sémaphores
Le projet Philosophers repose sur deux primitives de synchronisation fondamentales : les mutex et les sémaphores. Bien qu'elles puissent paraître interchangeables (un sémaphore initialisé à 1 se comporte comme un mutex), elles ont des sémantiques distinctes qui les rendent adaptées à des problèmes différents. Comprendre cette distinction est crucial pour la soutenance.
| Caractéristique | Mutex | Sémaphore |
|---|---|---|
| Nature | Verrou binaire (lock/unlock) | Compteur entier (nombre d'unités disponibles) |
| Usage principal | Exclusion mutuelle sur une ressource unique | Gestion de pools de ressources multiples ou signalisation |
| Orientation | Orienté thread (propriétaire) — seul le thread qui a locké peut unlocker | Non orienté thread — n'importe quel thread peut faire sem_post |
| Initialisation typique | Initialisé à 1 pour protéger une section critique | Peut être initialisé à N pour gérer N ressources |
| Performance | Plus simple, souvent plus performant pour l'exclusion mutuelle | Plus généraliste, potentiellement plus complexe à raisonner |
| Dans Philosophers | philo — chaque fourchette est un mutex | philo_two et philo_bonus — sémaphore par fourchette ou pool global |
Un mutex est un verrou qui protège une ressource : "je possède cette
fourchette, personne d'autre ne peut y toucher". Un sémaphore est un
signal qui coordonne des threads : "une fourchette est disponible,
celui qui la veut peut la prendre". La propriété d'ownership du mutex garantit
que seul le thread qui a locké peut unlocker — ce qui prévient les erreurs de
programmation. Avec un sémaphore, n'importe quel thread peut faire
sem_post, ce qui est utile pour la signalisation mais
dangereux pour l'exclusion mutuelle si mal utilisé.
Les trois erreurs de synchronisation
L'évaluation vérifie systématiquement la prévention de trois erreurs classiques de programmation concurrente. Chacune a un symptôme distinct et une solution spécifique :
| Erreur | Symptôme | Notre solution |
|---|---|---|
| Condition de course (data race) | Deux threads lisent/écrivent last_meal simultanément → corruption silencieuse, mort détectée trop tard ou jamais |
meal_mutex par philosophe protège last_meal et meals_eaten. Vérifié avec ThreadSanitizer (0 warnings). |
| Interblocage (deadlock) | Tous les philosophes prennent leur fourchette gauche simultanément → personne ne peut prendre la droite → programme figé | philo : ordre asymétrique (pairs vs impairs via left_fork/right_fork). philo_two / philo_bonus : décalage temporel (usleep(time_to_eat/2) pour les philosophes pairs) — pas de left_fork/right_fork car un seul sémaphore global. |
| Privation (starvation) | Un philosophe n'obtient jamais les fourchettes car ses voisins les monopolisent → meurt de faim alors que d'autres mangent | Le monitor scrute tous les philosophes toutes les 200µs. Si un philosophe dépasse time_to_die, il est déclaré mort et la simulation s'arrête — garantissant qu'aucune privation ne passe inaperçue. |
"Que se passerait-il si on supprimait le mutex sur last_meal ?"
Réponse : "Le monitor pourrait lire une valeur partiellement écrite (data race sur un
size_t 64 bits n'est pas atomique sur certaines architectures).
Résultat : la mort serait détectée trop tard, voire jamais — le délai < 10ms ne serait
plus garanti."
Sortie thread-safe : protéger stdout
Un aspect souvent sous-estimé mais systématiquement testé à la soutenance : la
cohérence de la sortie standard. Quand plusieurs threads (ou processus)
écrivent simultanément sur stdout via printf,
les messages peuvent s'entrelacer — produisant un flux illisible comme
"80000 314 i2s 0e0a tti0n 0g" au lieu de "800 3 is eating" et "800 2 is eating".
Le problème
printf n'est pas intrinsèquement thread-safe. La spécification C
ne garantit pas que l'écriture sur stdout est atomique. Deux threads écrivant
simultanément peuvent voir leurs buffers respectifs entrelacés au niveau du noyau.
Le résultat : des lignes mélangées, des timestamps incohérents, et un affichage
impossible à analyser pour le correcteur.
Notre solution : mutex d'affichage
Dans philo et philo_two, nous
utilisons un print_mutex (ou sémaphore print) partagé entre
tous les threads. Chaque appel à ph_print_status suit la
séquence stricte :
pthread_mutex_lock(&rules->print_mutex); // verrouiller
printf("%ld %d %s\n", timestamp, id, msg); // écrire le message complet
pthread_mutex_unlock(&rules->print_mutex); // déverrouiller
Cette approche sérialise les accès à stdout : chaque message est écrit entièrement
avant qu'un autre thread puisse commencer le sien. Le coût en performance est
négligeable car les printf sont peu fréquents (quelques
dizaines par seconde maximum).
Le cas philo_bonus : write() + sémaphore print
Dans philo_bonus (processus + fork), printf
pose un problème supplémentaire : le buffer libc est dupliqué à chaque fork(),
ce qui peut provoquer des affichages en double ou des flushes intempestifs. Notre solution
est double :
- Remplacer
printfpar des appels directs àwrite(1, ...)viaph_putstr_fdetph_putnbr_fd— cela élimine le buffering user-space dupliqué parfork(). - Garder le sémaphore
printautour de chaque affichage (sem_wait(print)/sem_post(print)) — car les multipleswrite()d'un même message (un par digit, séparateur, etc.) peuvent encore s'entrelacer entre processus sans cette protection.
write() est un appel système direct (sans buffer user-space), ce qui
élimine le problème de buffering dupliqué par fork(). Note : l'atomicité
POSIX PIPE_BUF ne s'applique qu'aux pipes/FIFOs, pas au terminal — d'où
la nécessité de garder le sémaphore print. printf()
utilise un buffer utilisateur en user-space — deux printf
concurrents peuvent interfolier leurs buffers avant le flush. Dans philo_bonus,
nous utilisons ph_putstr_fd et ph_putnbr_fd
qui appellent write directement, éliminant le problème de
buffering.
- Lancez
./philo 100 800 200 200— les lignes sont-elles lisibles ? - Aucune ligne ne doit être entrelacée ou tronquée
- Les timestamps doivent être strictement croissants
- Le message de mort doit être le dernier (le mutex d'affichage le garantit)
Scénarios de test et validation
La fiche d'évaluation impose des scénarios de test avec des paramètres précis, chacun conçu pour révéler une faille spécifique de la logique de synchronisation. Comprendre pourquoi chaque test existe est aussi important que le passer.
| Commande | Objectif | Résultat attendu | Ce que ça teste |
|---|---|---|---|
./philo 1 800 200 200 |
Philosophe unique | "801 1 died" (mort à ~800ms) | Le philosophe n'a qu'une fourchette → ne peut pas manger → meurt. Vérifie que le monitor détecte la mort dans le délai < 10ms. |
./philo 2 200 250 200 |
Test du délai < 10ms | Aucune mort | P1 mange à T=0 (last_meal=T0), mais à T201 le monitor détecte 201-0 > 200 et affiche '201 1 died'. Vérifie que le délai de détection est < 10ms (ici 1ms). |
./philo 4 310 200 100 |
Détection de mort (délai serré) | Un philosophe meurt | time_to_die=310 est juste assez pour manger une fois (200ms) + dormir (100ms). Un philosophe privé de fourchettes meurt. Teste la détection de mort fine. |
./philo 4 410 200 200 |
Survie sous pression | Aucune mort | time_to_die=410 permet deux cycles repas+sommeil (400ms). Vérifie que la synchronisation permet à tous de manger dans les temps. |
./philo 5 800 200 200 |
Stress standard | Aucune mort | Le test de référence. 5 philosophes, temps confortable. Vérifie l'absence de deadlock et de starvation sur la configuration standard. |
./philo 5 800 200 200 7 |
Repas limités (must_eat) | Simulation s'arrête après 7 repas par philosophe | Vérifie le compteur meals_eaten et l'arrêt propre quand must_eat est atteint. |
./philo 200 800 200 200 |
Stress intensif | Aucune mort (généralement) | 200 philosophes : le monitor doit scruter 200 × meal_mutex en < 10ms. Teste la scalabilité du monitor. |
Avec 200 philosophes, le monitor doit locker/délocker 200 meal_mutex
par cycle de scan. À ~50µs par mutex (lock + read + unlock), un scan complet prend ~10ms —
soit la limite exacte du délai autorisé. Sur un CPU lent ou sous charge, le monitor peut
dépasser 10ms et rater une détection de mort. C'est la limite fondamentale de notre
architecture à monitor unique. Une solution serait des monitors distribués (un par
philosophe) ou un flag volatile pour éviter le lock.
Méthodologie de test
Pour valider rigoureusement avant la soutenance, suivez cette méthodologie :
- Tests fonctionnels : lancez chaque scénario du tableau ci-dessus, vérifiez le résultat attendu.
- Tests de stress : lancez
./philo 5 800 200 200en boucle 100 fois — aucune mort ne doit survenir. - ThreadSanitizer : compilez avec
-fsanitize=threadet lancez./philo_tsan 5 800 200 200 7— 0 warnings attendus. - AddressSanitizer : compilez avec
-fsanitize=address— 0 leaks, 0 use-after-free. - Valgrind :
valgrind --leak-check=full ./philo 5 800 200 200 7— "no leaks are possible". - Sortie visuelle : vérifiez qu'aucune ligne n'est entrelacée, même avec 200 philosophes.
— 9S, analyse post-mission
Auto-évaluation & préparation soutenance
La soutenance de Philosophers ne se résume pas à montrer un programme qui fonctionne. L'évaluateur vérifie une compréhension profonde des concepts de programmation concurrente. Voici un cadre structuré pour l'auto-évaluation et la préparation.
Checklist threads
- Création : chaque thread a-t-il une fonction de routine bien définie ? Les arguments sont-ils correctement passés (structure sur le tas, pas sur la pile) ?
- Cycle de vie :
pthread_join()est-il appelé pour chaque thread ? Pas de fuite de threads ? - Terminaison propre : le
stopflag est-il vérifié à chaque itération de la routine ? Un thread peut-il se terminer proprement quand la simulation s'arrête ? - Mémoire partagée : toutes les variables partagées (
last_meal,meals_eaten,stop) sont-elles identifiées et protégées ?
Checklist synchronisation
- Choix de l'outil : pour chaque mutex/sémaphore, justifier son existence. Pourquoi un mutex ici et pas un sémaphore ?
- Paires lock/unlock : chaque
pthread_mutex_locka-t-il sonpthread_mutex_unlockcorrespondant ? Même dans les chemins d'erreur ? - Sections critiques courtes : les sections critiques sont-elles aussi courtes que possible ? Le
meal_mutexne protège quelast_mealetmeals_eaten, pas toute la routine. - Ordre des verrous : un ordre global est-il respecté ? Notre ordre asymétrique (pairs vs impairs) casse-t-il le cycle de deadlock ?
- Initialisation/destruction : tous les mutex sont initialisés (
PTHREAD_MUTEX_INITIALIZERoupthread_mutex_init) et détruits (pthread_mutex_destroy) ?
Checklist sortie
- Observation : lancez
./philo 50 800 200 200plusieurs fois — les messages sont-ils toujours lisibles ? - Mutex d'affichage : chaque
printfest-il encadré parlock/unlockdu print_mutex ? - Message de mort : le message "X died" est-il garanti d'être le dernier (le mutex d'affichage + le flag
stople garantissent) ?
Questions de soutenance typiques
| Question | Réponse attendue |
|---|---|
| "Pourquoi un mutex et pas un sémaphore pour les fourchettes dans philo ?" | Un mutex a une notion de propriétaire (le thread qui a locké doit unlocker), ce qui prévient les erreurs. Un sémaphore binaire n'a pas cette garantie — n'importe quel thread peut sem_post. Pour protéger une ressource unique (fourchette), le mutex est plus sûr et plus performant. |
| "Comment évitez-vous le deadlock ?" | Ordre asymétrique : les philosophes pairs prennent left_fork d'abord (classique droite), les impairs prennent left_fork d'abord (classique gauche). Deux philosophes adjacents veulent la même fourchette en premier → un seul la obtient → l'autre bloque sans rien tenir → pas de cycle circulaire. |
| "Que se passe-t-il si on supprime le meal_mutex ?" | Data race sur last_meal : le monitor pourrait lire une valeur partiellement écrite (sur architectures non 64-bit-atomiques). La mort serait détectée trop tard — le délai < 10ms ne serait plus garanti. ThreadSanitizer le détecterait comme warning. |
| "Pourquoi write() dans philo_bonus au lieu de printf() ?" | printf utilise un buffer user-space dupliqué à chaque fork() — les flushes peuvent produire des doublons. write() est un syscall direct, atomique pour ≤ PIPE_BUF (4096 bytes), sans buffering user-space. |
| "Comment garantissez-vous le délai < 10ms ?" | Le monitor dort 200µs entre chaque scan complet. Pour 5 philosophes, un scan prend ~250µs (5 × 50µs), soit un cycle ~450µs — largement sous 10ms. Pour 200 philosophes, le scan atteint ~10ms (limite). |
| "Pourquoi pas de variable volatile ?" | Les primitives de synchronisation (pthread_mutex_lock/unlock) garantissent la visibilité mémoire (barrière mémoire). volatile ne garantit l'atomicité ni l'ordonnancement — c'est un piège classique. Le seul cas légitime serait volatile sig_atomic_t pour les handlers de signaux, que nous n'utilisons pas. |
Structurez votre exposé en trois temps : (1) Architecture globale — montrez la structure des données et le flux de création des threads. (2) Synchronisation — expliquez le choix mutex vs sémaphore, l'ordre asymétrique anti-deadlock, et le monitor. (3) Démonstration — lancez les tests devant le correcteur, montrez la sortie lisible, et terminer par ThreadSanitizer (0 warnings). Anticipez les questions ci-dessus et préparez des réponses concises avec justification conceptuelle, pas juste technique.
— Commandant White, briefing pré-soutenance