hero-image

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 usleep inaccuracy with millisecond-accurate timing.

👤 Author

arri