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:Fork Initialization
Forks are initialized during setup ininit_forks:
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:
Fork Assignment Pattern
| Philosopher ID | Left Fork | Right Fork |
|---|---|---|
| 1 | Fork 0 | Fork N-1 |
| 2 | Fork 1 | Fork 0 |
| 3 | Fork 2 | Fork 1 |
| … | … | … |
| N | Fork N-1 | Fork 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:- Fork Access (
left_fork,right_fork): Ensures exclusive fork usage during eating - Death Flag (
lock_dead): Protects the shared termination flag - Meal Counter (
lock_meal): Protects philosopher meal count and last meal time - Write Operations (
lock_write): Serializes console output to prevent garbled messages
Shared Mutexes
The table structure initializes mutexes that are shared across all philosophers:The Eating Sequence
Theft_eat function demonstrates the core synchronization pattern for acquiring and releasing forks:
Step-by-Step Synchronization
-
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
-
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
-
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
-
Protected Eating
- Set
eating = 1flag - Lock
lock_mealto safely incrementmeal_counter - Update
last_meal_timetimestamp - Simulate eating with
ft_usleep(philo->time_to_eat)
- Set
-
Release Forks
- Set
eating = 0flag - Unlock left fork first
- Unlock right fork second
- Other philosophers can now acquire these forks
- Set
Preventing Race Conditions
Meal Counter Protection
The meal counter is a shared resource modified by philosopher threads and read by the monitor thread:- Philosopher thread increments
meal_counter - Monitor thread reads
meal_counterto check completion
Death Flag Protection
Thedead flag is checked by philosopher threads and set by the monitor thread:
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.Print Synchronization
Console output is serialized usinglock_write:
printf simultaneously would produce interleaved, garbled output.
Monitor Thread Synchronization
The monitor thread safely reads philosopher state using the same mutexes:Mutex Cleanup
While not shown in the provided source, proper cleanup requires destroying all mutexes: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