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 : les philosophes pairs prennent leur fourchette gauche puis droite, les impairs prennent droite puis gauche. Cela casse 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 : gauche puis droite */ philo->left_fork = &rules->forks[philo->id - 1]; philo->right_fork = &rules->forks[philo->id % rules->nb_philo]; } else { /* Impair : droite puis gauche (inverse le cycle) */ 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 detecté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_two_forks"); sem_unlink("/philo_two_print"); /* Sémaphore des fourchettes : initialisé à N (autant de fourchettes que de philos) */ rules->forks = sem_open("/philo_two_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_two_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) ne peut pas se produire : si N philosophes font chacun un
sem_wait, le compteur passe à 0, et le N+1ème
sem_wait bloque. Donc au maximum N fourchettes peuvent être
tenues simultanément — mais comme il en faut 2 pour manger, au moins un philosophe n'aura
qu'une seule fourchette et libérera l'autre. Le système se débloque tout seul.
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 toutes les 200µs. Avec 200 philosophes, chaque itération prend ~50µs (mutex lock/unlock par philo), donc le cycle complet prend ~250µs — largement sous les 10ms. Au prix d'une légère consommation CPU (0.5% avec 200 philos), on garantit une 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 à t=0, termine à t=250. Le philosophe 2 attend sa fourchette, et à
t=200 (time_to_die) il devrait mourir. Si le timestamp affiché est 201 ou 202, le délai est
de 1-2ms ✅. 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 detecté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