Skip to main content

Overview

Synchronization in the Philosophers simulation is achieved through POSIX mutexes (pthread_mutex_t). Mutexes prevent race conditions by ensuring that only one philosopher can access a shared resource (fork) at any given time.

Fork Representation

Each fork is represented as a mutex, with one mutex per fork:
typedef pthread_mutex_t t_mtx;
For N philosophers, there are exactly N forks (and thus N mutexes). This creates the fundamental resource scarcity that defines the dining philosophers problem.

Fork Initialization

Forks are initialized during setup in init_forks:
void init_forks(t_mtx *forks, int philo_num)
{
    int i;

    i = 0;
    while (i < philo_num)
    {
        pthread_mutex_init(&forks[i], NULL);
        i++;
    }
}
Each mutex is initialized with default attributes (NULL), creating a standard mutual exclusion lock. The number of mutexes equals the number of philosophers.

Fork Assignment

Each philosopher is assigned two fork pointers: left_fork and right_fork. The assignment logic in init_philos establishes the circular table arrangement:
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];

Fork Assignment Pattern

Philosopher IDLeft ForkRight Fork
1Fork 0Fork N-1
2Fork 1Fork 0
3Fork 2Fork 1
NFork N-1Fork N-2
Philosopher 1 (index 0) has a special case: their right fork is the last fork in the array, creating the circular table where each philosopher shares forks with their neighbors.

Mutex-Based Synchronization

Critical Sections

Mutexes protect several critical sections in the code:
  1. Fork Access (left_fork, right_fork): Ensures exclusive fork usage during eating
  2. Death Flag (lock_dead): Protects the shared termination flag
  3. Meal Counter (lock_meal): Protects philosopher meal count and last meal time
  4. Write Operations (lock_write): Serializes console output to prevent garbled messages

Shared Mutexes

The table structure initializes mutexes that are shared across all philosophers:
void init_data(t_table *table, t_philo *philos)
{
    table->dead_flag = 0;
    table->philos = philos;
    pthread_mutex_init(&table->lock_write, NULL);
    pthread_mutex_init(&table->lock_dead, NULL);
    pthread_mutex_init(&table->lock_meal, NULL);
}
Each philosopher receives pointers to these shared mutexes:
philos[i].lock_write = &table->lock_write;
philos[i].lock_dead = &table->lock_dead;
philos[i].lock_meal = &table->lock_meal;
philos[i].dead = &table->dead_flag;

The Eating Sequence

The ft_eat function demonstrates the core synchronization pattern for acquiring and releasing forks:
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);
}

Step-by-Step Synchronization

  1. Lock Right Fork (pthread_mutex_lock(philo->right_fork))
    • Philosopher attempts to acquire their right fork
    • If another philosopher holds it, this thread blocks until it becomes available
  2. Single Philosopher Check
    • Special case: With only 1 philosopher, there’s only 1 fork
    • The philosopher holds the fork, waits to die, then releases it
  3. Lock Left Fork (pthread_mutex_lock(philo->left_fork))
    • After acquiring the right fork, attempt to acquire the left fork
    • Both forks must be held before eating can begin
  4. Protected Eating
    • Set eating = 1 flag
    • Lock lock_meal to safely increment meal_counter
    • Update last_meal_time timestamp
    • Simulate eating with ft_usleep(philo->time_to_eat)
  5. Release Forks
    • Set eating = 0 flag
    • Unlock left fork first
    • Unlock right fork second
    • Other philosophers can now acquire these forks
Forks must be released in the reverse order they were acquired. While not strictly necessary in this implementation (since each fork is independent), it’s good practice and can prevent certain edge-case deadlocks in more complex scenarios.

Preventing Race Conditions

Meal Counter Protection

The meal counter is a shared resource modified by philosopher threads and read by the monitor thread:
pthread_mutex_lock(philo->lock_meal);
philo->meal_counter++;
pthread_mutex_unlock(philo->lock_meal);
Without the mutex, two operations could occur:
  1. Philosopher thread increments meal_counter
  2. Monitor thread reads meal_counter to check completion
These could race, leading to incorrect meal counts. The mutex ensures atomic read-modify-write operations.

Death Flag Protection

The dead flag is checked by philosopher threads and set by the monitor thread:
int dead_loop(t_philo *philo)
{
    pthread_mutex_lock(philo->lock_dead);
    if (*philo->dead == 1)
        return (pthread_mutex_unlock(philo->lock_dead), 1);
    pthread_mutex_unlock(philo->lock_dead);
    return (0);
}
Even reading a single integer (*philo->dead) requires mutex protection in multi-threaded environments. Without it, the compiler or CPU could cache the value, and philosopher threads might not observe the monitor’s update in a timely manner.
Console output is serialized using lock_write:
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);
}
Without this lock, multiple threads calling printf simultaneously would produce interleaved, garbled output.

Monitor Thread Synchronization

The monitor thread safely reads philosopher state using the same mutexes:
int philosopher_dead(t_philo *philo, size_t time_to_die)
{
    pthread_mutex_lock(philo->lock_meal);
    if (get_current_time() - philo->last_meal_time >= time_to_die
        && philo->eating == 0)
        return (pthread_mutex_unlock(philo->lock_meal), 1);
    pthread_mutex_unlock(philo->lock_meal);
    return (0);
}
The lock_meal mutex protects both meal_counter and the last_meal_time/eating state. This allows the monitor to atomically check if a philosopher has exceeded their time limit without eating.

Mutex Cleanup

While not shown in the provided source, proper cleanup requires destroying all mutexes:
// Pseudocode for cleanup
for (int i = 0; i < num_philos; i++)
    pthread_mutex_destroy(&forks[i]);
pthread_mutex_destroy(&table->lock_write);
pthread_mutex_destroy(&table->lock_dead);
pthread_mutex_destroy(&table->lock_meal);

Summary

The synchronization model uses:
  • N fork mutexes: One per fork, assigned to adjacent philosophers
  • 3 shared mutexes: For death flag, meal tracking, and output
  • Strict lock ordering: Right fork first, then left fork
  • Atomic operations: All shared data modifications are mutex-protected
  • Blocking behavior: Threads wait (block) when forks are unavailable rather than spinning

Build docs developers (and LLMs) love