Philosophers
Philosophers
A multithreaded take on Dijkstra’s dining philosophers problem. Every philosopher is its own thread, every fork (a knife, in this version) is a mutex, and the main thread doubles as a monitor that reports a death the moment it happens, all without a single malloc.
Features
- One Thread per Philosopher: Up to 200 philosophers eat, sleep and think in parallel.
- Deadlock-Free: Knives are always locked in the same global order, so no circular wait can form.
- Staggered Start: Odd philosophers begin by sleeping, so the even ones eat first without a fight.
- Precise Timing: A custom sleep function keeps every action close to its intended timestamp.
- Zero Heap: Philosophers, knives and thread IDs live in fixed-size arrays on the stack. Nothing to free, nothing to leak.
Implementation Highlights
1. Deadlock-Free Knife Grabbing
If every philosopher grabbed their left knife first, they could all end up holding one knife and waiting forever for the second. grab_em avoids this with Dijkstra’s resource hierarchy: always lock the knife with the lower memory address first. With one global lock order, a circular wait is impossible.
static void grab_em(t_philo *philo)
{
if (philo->l_mtx < philo->r_mtx) // Lower address first
{
pthread_mutex_lock(philo->l_mtx);
printer(philo, "has taken a knife(scary)");
pthread_mutex_lock(philo->r_mtx);
printer(philo, "has taken a knife(scary)");
}
else
{
pthread_mutex_lock(philo->r_mtx);
printer(philo, "has taken a knife(scary)");
pthread_mutex_lock(philo->l_mtx);
printer(philo, "has taken a knife(scary)");
}
}
2. The Monitor & a Clean Death
After creating the threads, the main thread becomes the monitor. It loops over the philosophers without pausing and, under nom_mtx, checks how long ago each one last ate. The died line is printed and the death flag is set while holding print_mtx. Since printer checks that flag under the same lock, no message can ever appear after a death.
void printer(t_philo *philo, char *msg)
{
size_t now;
pthread_mutex_lock(&philo->env->print_mtx);
now = get_time() - philo->env->times.start;
if (!died(philo->env)) // Silent once someone died
printf("%lu %d %s\n", now, philo->id, msg);
pthread_mutex_unlock(&philo->env->print_mtx);
}
void print_death(t_philo *philo)
{
pthread_mutex_lock(&philo->env->print_mtx);
printf("%lu %d died\n", get_time() - philo->env->times.start, philo->id);
death(philo->env); // Flag is set before anyone else can print
pthread_mutex_unlock(&philo->env->print_mtx);
}
3. Precise Sleep
usleep can oversleep, and over hundreds of cycles the error adds up until a philosopher starves. eepy sleeps in short 100 µs steps and checks the clock after each one, so it wakes up right on schedule.
void eepy(size_t ms)
{
size_t bed_time;
bed_time = get_time();
while (ms > (get_time() - bed_time))
usleep(100);
}
Usage
1. Compilation
cd philo && make
2. Run
./philo number_of_philosophers time_to_die time_to_eat time_to_sleep [meals]
3. Examples
./philo 5 800 200 200 # Nobody dies, runs until Ctrl+C
./philo 5 800 200 200 7 # Stops once everyone has eaten 7 times
./philo 4 310 200 100 # A philosopher dies at ~310 ms
Skills Learned
- Multithreading: Creating and joining POSIX threads (
pthread_create,pthread_join). - Synchronization: Protecting shared data with mutexes to prevent data races.
- Deadlock Prevention: Breaking circular waits with a fixed lock order.
- Timing Precision: Working around
usleepinaccuracy with millisecond-accurate timing.
👤 Author
- GitHub: cuteKittenArri