Loading Project....
The Dining Philosophers Problem is a classic problem in computer science used to illustrate challenges in concurrent programming, particularly resource allocation and the avoidance of deadlocks and starvation. This project provides a robust C implementation that simulates this problem using POSIX threads and mutexes.
It serves as an excellent educational tool for understanding fundamental concepts of concurrency, thread synchronization, and strategies to manage shared resources effectively in a multi-threaded environment. By observing the philosophers' behavior, developers can gain insights into how race conditions and deadlocks can arise and how they can be systematically prevented.
To build and run this project, you will need the following software installed on your system:
Follow these steps to get a local copy of the project up and running on your machine.
Clone the repository and navigate into the project directory:
git clone https://github.com/TioElvis/philosophers.git
cd philosophers
Compile the project using make:
make all
This command will compile all source files and create an executable named philosophers in the root directory. You should see a success message with some ASCII art upon successful compilation.
Once compiled, you can run the simulation from your terminal. The program requires several command-line arguments to define the simulation parameters.
./philosophers <number_of_philosophers> <time_to_die> <time_to_eat> <time_to_sleep> [number_of_meals]
To run a simulation with 5 philosophers, where a philosopher dies after 800ms without eating, eats for 200ms, and sleeps for 200ms:
./philosophers 5 800 200 200
0 1 is thinking
0 2 is thinking
0 3 is thinking
0 4 is thinking
0 5 is thinking
10 2 has taken fork
10 2 has taken fork
10 2 is eating
10 4 has taken fork
10 4 has taken fork
10 4 is eating
210 1 has taken fork
210 1 has taken fork
210 1 is eating
210 2 is sleeping
210 3 has taken fork
210 3 has taken fork
210 3 is eating
...
The behavior of the philosophers simulation is controlled by command-line arguments. All arguments must be positive integers greater than 0 and within the range of an int.
number_of_philosophers: The total number of philosophers sitting at the table. This also determines the number of forks available.time_to_die: The maximum time (in milliseconds) a philosopher can go without eating before they are considered to have died.time_to_eat: The time (in milliseconds) a philosopher spends eating.time_to_sleep: The time (in milliseconds) a philosopher spends sleeping.[number_of_meals] (optional): If provided, the simulation will terminate once every philosopher has eaten at least this many times. If omitted, the simulation continues until a philosopher dies.The project is organized into a src directory containing all C source files and a Makefile for compilation.
.
├── Makefile
└── src
├── cleanup.c
├── initialize_args.c
├── initialize_table.c
├── main.c
├── monitor.c
├── philosophers.h
├── routine.c
├── start_dinner.c
└── utils.c
Makefile: Manages the build process, including compilation, linking, and cleaning.src/philosophers.h: The main header file defining data structures (t_philo, t_args, t_table), enums (e_bool, e_state), and function prototypes.src/main.c: The entry point of the program, responsible for argument parsing, initialization, starting the simulation, and cleanup.src/initialize_args.c: Contains functions to parse and validate command-line arguments.src/initialize_table.c: Handles the allocation and initialization of the t_table structure, including philosophers and mutexes for forks.src/start_dinner.c: Orchestrates the creation of philosopher threads and the monitor thread, and manages their joining.src/routine.c: Implements the core actions of a philosopher: thinking, eating, and sleeping, including the logic for acquiring forks.src/monitor.c: Contains the monitor_routine which periodically checks the state of philosophers to detect starvation or if all meals have been eaten.src/cleanup.c: Responsible for destroying mutexes and freeing dynamically allocated memory.src/utils.c: Provides utility functions such as gettimeofday_ms for precise timing, delay for pausing execution, print_state for thread-safe logging, and is_finished_dinner for checking global simulation status.This project is a practical demonstration of several key concepts in concurrent programming.
The problem describes five philosophers sitting around a circular table, with a bowl of spaghetti in the center. Between each pair of philosophers is a single fork. To eat, a philosopher needs two forks: one from their left and one from their right. Philosophers alternate between thinking, eating, and sleeping. The challenge is to design a protocol that allows philosophers to eat without causing a deadlock (where no one can eat) or starvation (where some philosophers never get to eat).
The simulation uses POSIX threads (pthread) to represent each philosopher and a dedicated monitor. pthread_mutex_t are used for synchronization:
philo_routine (eating, sleeping, thinking).monitor_routine) continuously checks the state of all philosophers to determine if the simulation should end due to starvation or meal completion.pthread_mutex_t. A philosopher must successfully lock two adjacent fork mutexes to eat.display mutex (table->display) ensures that output to the console (printf) is thread-safe and readable, preventing interleaved messages.meal and sleep mutexes to protect last_meal_time, meals_eaten, and last_sleep_time from race conditions when accessed by the monitor thread.A common pitfall in the Dining Philosophers Problem is deadlock, which occurs if all philosophers simultaneously pick up their left fork, then wait indefinitely for their right fork. This project implements a strategy to prevent this:
This breaks the circular wait condition, ensuring that at least one philosopher can always acquire both forks and eat, thus preventing a global deadlock. The take_forks_even and take_forks_odd functions in src/routine.c encapsulate this logic.
// From src/routine.c
static int take_forks_even(t_philo *philo)
{
pthread_mutex_lock(philo->left_fork);
print_state(philo, "has taken fork");
// ... error handling and termination checks ...
pthread_mutex_lock(philo->right_fork);
print_state(philo, "has taken fork");
// ... error handling and termination checks ...
return (SUCCESS);
}
static int take_forks_odd(t_philo *philo)
{
pthread_mutex_lock(philo->right_fork);
print_state(philo, "has taken fork");
// ... error handling and termination checks ...
pthread_mutex_lock(philo->left_fork);
print_state(philo, "has taken fork");
// ... error handling and termination checks ...
return (SUCCESS);
}
A dedicated monitor_routine runs in parallel to oversee the simulation. Its responsibilities include:
last_meal_time against time_to_die. If a philosopher has not eaten within time_to_die milliseconds, they are declared DEAD, and the simulation terminates.number_of_meals is specified, the monitor tracks meals_eaten for each philosopher. Once all philosophers have eaten the required number of meals, the simulation gracefully terminates.is_finished flag within the t_table structure, protected by table->monitor mutex, is set to TRUE when either a philosopher dies or all meals are consumed. This flag is checked by all philosopher threads to ensure they stop their routines promptly.The simulation provides a clear and observable representation of the Dining Philosophers Problem.
Each philosopher thread cycles through the following states:
last_meal_time and meals_eaten count. After time_to_eat milliseconds, they release the forks.time_to_sleep milliseconds.This cycle continues until the simulation terminates.
The simulation uses custom utility functions for time tracking and delays to ensure consistent behavior:
gettimeofday_ms(): Returns the current time in milliseconds since the epoch, used for precise timestamping of events.delay(long ms): A custom busy-wait function that pauses execution for a specified number of milliseconds. This is used to simulate the time_to_eat and time_to_sleep durations.// From src/utils.c
long gettimeofday_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (tv.tv_sec * 1000 + tv.tv_usec / 1000);
}
void delay(long ms)
{
long start_time;
start_time = gettimeofday_ms();
while (gettimeofday_ms() - start_time < ms)
usleep(100);
}
To ensure that messages from different philosophers and the monitor are printed cleanly to the console without being garbled, a display mutex is used. The print_state function acquires this mutex before printing and releases it afterward.
// From src/utils.c
void print_state(t_philo *philo, const char *state)
{
long timestamp;
pthread_mutex_lock(&philo->table->display);
timestamp = gettimeofday_ms() - philo->table->start_time;
if (is_finished_dinner(philo->table) == TRUE)
{
pthread_mutex_unlock(&philo->table->display);
return ;
}
printf("%ld %d %s\n", timestamp, philo->id, state);
pthread_mutex_unlock(&philo->table->display);
}
This ensures that each state change (e.g., "is thinking", "has taken fork", "is eating", "is sleeping", "died") is printed as a complete line, prefixed with the timestamp and philosopher ID.
The Makefile provides several convenient commands for managing the project:
make all (or just make): Compiles all source files and links them to create the philosophers executable.make clean: Removes all intermediate object files (.o) generated during compilation.make fclean: Removes all object files and the final philosophers executable.make re: Performs a full clean (fclean) followed by a complete rebuild (all). This is useful for ensuring a fresh compilation.