Skip to main content

Overview

Philosophers perform three core actions in a continuous cycle: eating, sleeping, and thinking. Each action is implemented as a function with careful mutex handling to ensure thread safety and prevent deadlocks.

The Eat Action

ft_eat Function

void    ft_eat(t_philo *philo)
{
    pthread_mutex_lock(philo->right_fork);
    print_message("has taken a fork", philo, philo->id);
    if (philo->nbr_philo == 1)
    {
        ft_usleep(philo->time_to_die);
        pthread_mutex_unlock(philo->right_fork);
        return ;
    }
    pthread_mutex_lock(philo->left_fork);
    print_message("has taken a fork", philo, philo->id);
    philo->eating = 1;
    print_message("is eating", philo, philo->id);
    pthread_mutex_lock(philo->lock_meal);
    philo->meal_counter++;
    pthread_mutex_unlock(philo->lock_meal);
    philo->last_meal_time = get_current_time();
    ft_usleep(philo->time_to_eat);
    philo->eating = 0;
    pthread_mutex_unlock(philo->left_fork);
    pthread_mutex_unlock(philo->right_fork);
}
Source: philo_actions.c:26

Fork Acquisition Sequence

1

Acquire Right Fork

pthread_mutex_lock(philo->right_fork);
print_message("has taken a fork", philo, philo->id);
The philosopher locks their right fork first and announces it.
2

Handle Single Philosopher Case

if (philo->nbr_philo == 1)
{
    ft_usleep(philo->time_to_die);
    pthread_mutex_unlock(philo->right_fork);
    return ;
}
Special case: With only one philosopher and one fork, eating is impossible. Wait for time_to_die, then release the fork and return.
3

Acquire Left Fork

pthread_mutex_lock(philo->left_fork);
print_message("has taken a fork", philo, philo->id);
Lock the left fork and announce acquisition.
All philosophers acquire their right fork first, then their left fork. This consistent ordering is critical for deadlock prevention.

Why Right Fork First?

The fork assignment in init_philos creates an asymmetric pattern:
philos[i].left_fork = &forks[i];
if (i == 0)
    philos[i].right_fork = &forks[philos[i].nbr_philo - 1];
else
    philos[i].right_fork = &forks[i - 1];
Result:
  • Philosopher 1: left=fork[0], right=fork[n-1]
  • Philosopher 2: left=fork[1], right=fork[0]
  • Philosopher 3: left=fork[2], right=fork[1]
By taking the right fork first:
  • Most philosophers take a “lower-indexed” fork first
  • The first philosopher takes the “highest-indexed” fork first
  • This breaks the circular wait condition that causes deadlock
If all philosophers tried to take their left fork first:
  1. All philosophers grab their left fork simultaneously
  2. All philosophers wait for their right fork (held by neighbor)
  3. Circular wait → deadlock
The right-first strategy prevents this by creating an ordering that breaks the cycle.

Eating Process

State Updates

Once both forks are acquired:
philo->eating = 1;
print_message("is eating", philo, philo->id);

pthread_mutex_lock(philo->lock_meal);
philo->meal_counter++;
pthread_mutex_unlock(philo->lock_meal);

philo->last_meal_time = get_current_time();
ft_usleep(philo->time_to_eat);
philo->eating = 0;
philo->eating = 1;
Marks the philosopher as currently eating. The monitor thread checks this flag to avoid false death detection.

Fork Release

pthread_mutex_unlock(philo->left_fork);
pthread_mutex_unlock(philo->right_fork);
Forks are released in the opposite order of acquisition (left then right). While not strictly necessary for correctness in this case, it’s good practice to release locks in reverse order.
The meal_counter increment is protected by lock_meal, but last_meal_time and eating are not locked during their updates in ft_eat. This is safe because:
  • eating is set to 1 before the monitor could detect it
  • last_meal_time is updated while eating=1, preventing false deaths
  • Only this philosopher writes these values

The Sleep Action

ft_sleep Function

void    ft_sleep(t_philo *philo)
{
    print_message("is sleeping", philo, philo->id);
    ft_usleep(philo->time_to_sleep);
}
Source: philo_actions.c:20

Purpose

  • Simulates the philosopher’s rest period after eating
  • No mutexes needed (no shared state access)
  • Duration specified by time_to_sleep argument
The sleeping state gives other philosophers a chance to acquire forks. Without sleeping, a single philosopher might monopolize both their forks by immediately trying to eat again.

The Think Action

ft_think Function

void    ft_think(t_philo *philo)
{
    print_message("is thinking", philo, philo->id);
}
Source: philo_actions.c:15

Why No Delay?

Unlike eating and sleeping, thinking has no explicit duration:
  • The philosopher immediately tries to acquire forks after thinking
  • The “thinking time” is implicitly the time spent waiting for forks
  • This matches the problem description: philosophers think until hungry
When a philosopher calls ft_think and then ft_eat, they will block on:
pthread_mutex_lock(philo->right_fork);
This blocking wait is the “thinking” period—the philosopher thinks until both forks become available.

The print_message Function

All actions use print_message to output state changes:
void    print_message(char *str, t_philo *philo, int id)
{
    size_t  time;

    pthread_mutex_lock(philo->lock_write);
    time = get_current_time() - philo->start_time;
    if (!dead_loop(philo))
        printf("%zu %d %s\n", time, id, str);
    pthread_mutex_unlock(philo->lock_write);
}
Source: monitor.c:15

Output Format

<timestamp_ms> <philosopher_id> <message>
Example:
0 1 has taken a fork
0 1 has taken a fork
0 1 is eating
200 1 is sleeping
400 1 is thinking

Thread Safety

pthread_mutex_lock(philo->lock_write);
// ... print ...
pthread_mutex_unlock(philo->lock_write);
The lock_write mutex ensures that output from multiple philosophers doesn’t interleave, preventing garbled messages.
print_message calls dead_loop, which locks lock_dead. This means two mutexes may be held simultaneously (lock_write and lock_dead). This is safe because the order is always consistent: lock_write first, then lock_dead (inside dead_loop).

Timing Utilities

Actions rely on timing functions from time_utils.c:

get_current_time

Returns the current time in milliseconds (typically since epoch or simulation start).

ft_usleep

A more precise sleep function:
  • Takes duration in milliseconds
  • Uses usleep or busy-waiting for accuracy
  • Critical for meeting tight timing requirements
Standard usleep can be imprecise. The implementation may use a combination of sleep and busy-waiting to achieve accurate delays for time_to_eat and time_to_sleep.

Action Cycle Summary

Philosopher Action Cycle:

     ┌─────────────┐
     │  ft_think   │
     │  (no delay) │
     └──────┬──────┘

            v
     ┌─────────────────────┐
     │     ft_eat          │
     │ ┌─────────────────┐ │
     │ │ Lock right fork │ │
     │ │ Lock left fork  │ │
     │ │ eating = 1      │ │
     │ │ meal_counter++  │ │
     │ │ Update time     │ │
     │ │ Sleep(eat_time) │ │
     │ │ eating = 0      │ │
     │ │ Unlock forks    │ │
     │ └─────────────────┘ │
     └──────┬──────────────┘

            v
     ┌─────────────────┐
     │   ft_sleep      │
     │ Sleep(sleep_time)│
     └──────┬──────────┘

            └──────┐

     ┌─────────────v──┐
     │  dead_loop()?  │
     └────┬──────┬────┘
         No     Yes
          │      │
          │      v
          │   [Exit]

          └────>(Loop back to ft_think)
This cycle continues until the monitor sets the dead flag, at which point dead_loop returns true and the philosopher thread exits.

Build docs developers (and LLMs) love