This guide breaks down the key steps involved in the Philosophers project. The goal is to simulate the classic "Dining Philosophers" computer science problem, focusing on concurrency, resource sharing, and avoiding issues like deadlock and starvation.
- Scenario: Imagine several philosophers sitting around a circular table. Between each pair of adjacent philosophers lies a single fork.
- Goal: Philosophers alternate between thinking, getting hungry, eating, and sleeping.
- Challenge: To eat, a philosopher needs two forks – the one on their left and the one on their right. They must pick up both forks without causing a situation where no one can eat (deadlock) or where a specific philosopher never gets a chance to eat (starvation).
- Parsing Input (
philo/parse.c,main.c):- The program starts by reading command-line arguments:
- Number of philosophers
- Time to die (ms): Max time a philosopher can go without eating.
- Time to eat (ms): Duration of eating.
- Time to sleep (ms): Duration of sleeping.
- (Optional) Number of times each philosopher must eat.
- Functions like
parse_arguments,check_limits,ft_atoi, andft_isnumberhandle reading and validating this input. Invalid input leads to errors handled inphilo/errors.c.
- The program starts by reading command-line arguments:
- Allocating Memory (
philo/allocate.c,philo/memory_management.c):- Memory is needed to store information about the simulation (
t_simulation), the table (t_table), each philosopher (t_philosopher), and each fork (t_fork). These structures are defined inphilo/philo.h. - Functions like
allocate_simulation,allocate_forks,allocate_philosophers, andft_callocreserve the necessary memory.
- Memory is needed to store information about the simulation (
- Initialization (
philo/allocate.c,philo/parse.c):- Each philosopher and fork is initialized with its ID and default state.
- Crucially: A mutex (
pthread_mutex_t) is created for each fork (init_forks) and each philosopher (init_philosophers). Mutexes act like locks to protect shared resources (like forks or philosopher data) from being accessed by multiple threads simultaneously. - Global mutexes (e.g., for logging
log_mutex, checking deathdeath_mutex) are also initialized insetup_simulation.
- Threads: Each philosopher runs concurrently in its own thread, executing the
philosopher_routinefunction. - The Cycle: Inside the loop (
while (!is_simulation_over(...))), a philosopher performs actions:- Think (
think): Logs the thinking status. - Take Forks (
take_forks,philo/action_utils.c):- Attempts to acquire the mutexes for both the left and right forks using
try_take_forkwhich callslock_safely(philo/utils2.c). - To prevent deadlock, philosophers might pick up forks in a specific order (e.g., even/odd IDs pick up left/right first, or lower ID fork first). The
sidevariable and potentialusleep(DELAY_AFTER_CREATION)help stagger attempts. - The philosopher's
left_forkandright_forkvariables track which forks they hold.
- Attempts to acquire the mutexes for both the left and right forks using
- Eat (
eat):- If successful in getting both forks:
- Logs "is eating".
- Updates their
last_meal_timeand calculatesmeal_end_timeandwake_up_time(set_philo_timesinphilo/setters.c). - Simulates eating using
sleep_till(philo/time.c). - Releases the forks using
release_fork(which callsunlock_safely). - Increments their meal count (
set_eaten_meals).
- If successful in getting both forks:
- Sleep (
get_a_nap):- Logs "is sleeping".
- Simulates sleeping using
sleep_till.
- Think (
- Checking Status: Philosophers constantly check if the simulation has ended (
is_simulation_overinphilo/utils.c) or if they are still alive (im_aliveinphilo/death.c).
- Monitor Thread(s): One or more separate threads run the
monitoring_routine. Using multiple monitors (get_nbr_chuncksinphilo/utils2.c) can improve efficiency for large numbers of philosophers. - Health Checks:
- The monitor periodically checks each philosopher it's responsible for.
- It safely reads the philosopher's
last_meal_timeandtimes_eatenusingget_philo_data(philo/getters.c), which locks the philosopher's specific mutex.
- Detecting Death:
- Calculates time since the last meal (
current_time_usinphilo/time.c). - If this exceeds
time_to_die(is_aliveinphilo/death.c), it callsreport_death, which sets a globalsomeone_diedflag (protected bydeath_mutexviaset_someone_died) and logs the death.
- Calculates time since the last meal (
- Checking Meal Count: The monitor also checks if all philosophers have completed their required meals using
dinner_is_over(philo/utils.c).
- The Scribe Thread: A dedicated thread runs
scribe_routine. Its purpose is to print status messages from all philosophers in the correct chronological order. - Recording: When a philosopher acts,
log_action(philo/utils.c) creates a log entry (t_log) with the timestamp, philosopher ID, and message. - Storing: These logs are added to a shared linked list (
log_lstint_simulation), sorted by timestamp. Access is protected bylog_mutex(add_log). - Printing: The scribe periodically wakes up (
sleep_ms), locks thelog_mutex, prints logs that occurred sufficiently in the past (print_logs,display_log), removes them from the list, and unlocks the mutex. This ensures output isn't jumbled due to thread concurrency.
- Orchestration: The
run_simulationfunction manages the overall process. - Thread Creation: It uses
pthread_createto start all philosopher threads, monitor thread(s), and the scribe thread. - Waiting: It uses
pthread_jointo wait for all threads to complete. Philosophers finish when the simulation ends (death or meals completed). Monitors and the scribe finish when they detect the simulation end condition.
- Resource Release: After the simulation ends (
main.c), it's crucial to clean up:destroy_mutexes: Destroys all initialized mutexes.free_simulation: Frees all dynamically allocated memory (for philosophers, forks, table, logs, etc.) to prevent memory leaks.
- The
Makefileprovides commands to compile the source code (.cfiles) into an executable program (philo). - Common commands:
make: Compiles the project.make clean: Removes intermediate object files (.o).make fclean: Removes object files and the executable.make re: Recompiles everything cleanly.make run_test,make valgrind,make helgrind: Useful targets for running tests and checking for memory or threading errors.
- Threads: Allow philosophers (and monitors/scribe) to run concurrently.
- Mutexes: Essential locks to protect shared data (forks, philosopher state, logs) from race conditions, ensuring data integrity.
- Deadlock: A state where threads are blocked forever, waiting for resources held by each other. Prevented by careful resource acquisition strategies (e.g., fork ordering).
- Starvation: A state where a thread is perpetually denied access to necessary resources and cannot make progress. Monitored by checking
time_to_die.