- The philosphers project breakdown.
- Theory breakdown for better understanding.
- Tips based on my experince doing this project for people wanting to do it as well.
The Dining Philosophers Problem is a classic computer science thought experiment that illustrates the challenges of synchronizing concurrent processes. It depicts a group of silent philosophers sitting at a round table who alternate between three states: eating, sleeping, and thinking. The catch is that there is only one fork between each pair of neighbors, but a philosopher needs two forks to eat their spaghetti. This creates a fierce competition for shared resources, serving as a direct metaphor for threads accessing shared memory in an operating system. The ultimate goal is to design an algorithm that allows everyone to eat without causing a Deadlock (where everyone holds one fork and waits forever) or Starvation (where a philosopher dies waiting).
The simulation is governed by a strict set of rules and arguments that define the behavior of the philosophers and their environment.
The program must be executed with the following parameters:
./philo number_of_philosophers time_to_die time_to_eat time_to_sleep [number_of_meals]| Argument | Unit | Description |
|---|---|---|
number_of_philosophers |
— | The number of philosophers and also the number of forks available. |
time_to_die |
ms | If a philosopher does not start eating within this timeframe (since the start of their last meal or the simulation start), they die. |
time_to_eat |
ms | The time it takes for a philosopher to eat. They will hold two forks during this period. |
time_to_sleep |
ms | The time a philosopher spends sleeping. |
[number_of_meals] |
— | (Optional) If all philosophers eat at least this many times, the simulation ends. If not specified, the simulation stops only upon death. |
- Philosophers sit at a round table
- Philosopher
1sits next to PhilosopherN - Any Philosopher
Nsits betweenN-1andN+1
- There is one fork between each pair of philosophers
-
To eat, a philosopher must successfully acquire two forks:
- One on their left
- One on their right
- If there is only 1 philosopher, there is only 1 fork
- The philosopher cannot eat and will inevitably die
The program must print state changes in a strict, non-scrambled format. Each log entry includes:
- A timestamp (in milliseconds, relative to the simulation start)
- The philosopher's ID
timestamp_in_ms X has taken a fork
timestamp_in_ms X is eating
timestamp_in_ms X is sleeping
timestamp_in_ms X is thinking
timestamp_in_ms X died
- Each philosopher is a thread
- Each fork is a mutex
- The message announcing a philosopher’s death must be printed no later than 10ms after the actual death event
- Output must not be interleaved or scrambled
- Guaranteed via a dedicated
print_lockmutex
Understanding the core concepts of concurrency is essential to grasp how this simulation works.
The project is built upon three pillars of operating system theory. If you are new to this project or this world
Please read the theory part for more context and how it links with my implemnetation:
A thread is the smallest unit of execution within a process.
Unlike separate processes (which have isolated memory), threads share the same memory space and resources.
- Each Philosopher runs as a secondary thread
- The Monitor runs on the Main Thread
- The Main Thread is the parent
- If the Main Thread terminates before secondary threads finish:
- The entire process exits
- All philosopher threads are killed mid-execution
We use pthread_join to ensure:
- The Main Thread waits for all philosophers to finish
- Cleanup and exit happen only after all threads return
The runtime behavior depends on the hardware:
- On multi-core CPUs
- Threads truly execute at the same time
- On single-core CPUs
- Or when threads > CPU cores
- The CPU rapidly switches between threads
- Creates the illusion of simultaneous execution
⚠️ Note:
Threads introduce overhead. While they are necessary here, using threads for simple sequential tasks is often inefficient due to the cost of context switching.
Because threads share memory, a Race Condition occurs when:
- Two threads read/write the same variable at the same time
- The result depends on the unpredictable execution order
If two philosophers grab Fork 1 simultaneously without protection:
- Both may believe they own the fork
- This leads to duplicated resources and undefined behavior
A Mutex (Mutual Exclusion lock) protects shared resources.
- Lock: A thread takes the key — no one else can enter
- Action: The resource is safely modified
- Unlock: The key is returned — next thread may enter
- Forks: Ensure only one philosopher holds a fork
- Writing (
print_lock): Prevents log interleaving - Reading (
dead_lock,meal_lock): Ensures the Monitor reads consistent data
To fully master this project, you must understand:
- The boundary between User Space and Kernel Space
- The mathematical conditions that create deadlocks
A common misconception:
pthread_tis not the thread itself.
It is merely a handle — a remote control.
The value you receive in a variable of type pthread_t is not the thread itself, but only a thread identifier (a handle).
This identifier allows you to refer to the thread in function calls such as:
pthread_join()pthread_detach()pthread_cancel()
but it does not contain the actual execution of the thread.
A pthread_t is similar to:
- A pointer
- A token
- A remote control
The operating system gives it to you so you can manage and interact with the thread, but the real thread (its execution context, stack, and scheduling) lives inside the kernel, not in your variable.
In short:
pthread_tis a reference to a thread, not the thread itself.
data->threadsis just an array of thread identifiers- Allocated with
malloc
- The real thread lives here:
- Execution context
- Stack
- Registers
- Scheduling state
If you call:
free(data->threads);without calling pthread_join:
- ❌ You destroy the remote control
- ✅ The OS thread keeps running
- ❌ You lose the ability to manage or stop it
- Threads continue accessing mutexes or memory
- That memory may already be freed
- 💥 Segmentation Fault
-
malloc / free:- Manages User Space
- You own it → free destroys it immediately
-
pthread_create:- Allocates resources in Kernel Space
free()only removes your handlepthread_join()tells the Kernel to clean up the real thread
A Deadlock is not random — it occurs only if all four conditions are met simultaneously.
Break one, and deadlock becomes impossible.
-
Mutual Exclusion A resource (fork) can only be held by one philosopher → ❌ Cannot change (forks are physical)
-
Hold and Wait A philosopher holds one fork while waiting for another → ❌ Cannot change (eating requires two forks)
-
No Preemption Resources cannot be forcibly taken → ❌ Cannot steal forks
-
Circular Wait A closed chain where each philosopher waits for the next
We eliminate the 4th condition.
Philosophers must always acquire forks in a global order (Lowest ID fork first)
- The circular dependency becomes mathematically impossible
- The last philosopher will never hold a high-ID fork while waiting on a low-ID fork
- The chain cannot close → no deadlock
🧠 Key Insight: Deadlocks are not timing bugs. They are logical states — and logic can be broken deterministically.
The program entry point (main.c) is designed with a high-level abstraction philosophy. It avoids cluttering the main logic with implementation details, delegating tasks to three distinct stages: Validation, Initialization, and Execution.
int main(int argc, char **argv)
{
// 1. Parse and validate inputs (Numbers only, valid ranges)
if (validate_args(argc, argv) == ERROR) ...
// 2. Allocate memory and initialize mutexes
if (init_data(&data, argc, argv) == ERROR) ...
// 3. Start threads and monitor loop
if (choose_routine_type(&data) == ERROR) ...
return (0);
}The initialization phase is strictly modularized to ensure memory safety. If any step fails (malloc failure or mutex init failure), a custom clean_program function is triggered to free only the resources allocated up to that point.
The setup is divided into four key components:
-
Allocates memory for the
t_philoarray -
Assigns unique IDs
-
Crucial: Initializes a specific
meal_lockfor each philosopher- Protects the
last_meal_timevariable - Prevents data races between the Philosopher thread (writing) and the Monitor thread (reading)
- Protects the
-
Allocates the array of forks (mutexes)
-
Initializes global mutexes:
print_lock: Ensures console output is not interleaved/scrambleddead_lock: Protects the simulation status (philo_is_deadflag)
-
Converts time arguments to
u_int64_t -
Handles the optional Number of Meals argument
- If not provided, the simulation runs indefinitely (
NOT_ACTIVE)
- If not provided, the simulation runs indefinitely (
To simulate the round table, pointers for the Left and Right forks are assigned using modular arithmetic. This connects the last philosopher back to the first, closing the ring.
// i = current philosopher ID
// n = total number of philosophers
philos[i].left_side_fork = &forks[i];
philos[i].right_side_fork = &forks[(i + 1) % n];- Left Fork: The fork at the philosopher's current index
- Right Fork: The fork at the next index (wraps to
0for the last philosopher)
Instead of a generic “free all” approach, clean_program acts as a surgical destructor. It accepts parameters that dictate exactly which resources have been allocated and need to be released.
void clean_program(t_dinning_info *data, int last_mutex_to_failed, int globa)If initialization of the 50th fork fails (in a simulation of 100), a naive cleanup would crash if it attempted to destroy the 51st mutex (never initialized).
Solution:
- Iterate from
0tolast_mutex_to_failed - Call
pthread_mutex_destroyonly on valid, initialized mutexes - Simultaneously destroy the
meal_lockassociated with each philosopher
A Level System tracks the initialization stage of global locks:
- Level 1 (
globa >= 1): Destroysprint_lock - Level 2 (
globa == 2): Destroysdead_lock
This prevents destroying mutexes that were never created.
- Pointers (
data->forks,data->philos,data->threads) are checked forNULLbefore callingfree() - Prevents double-free errors and freeing unallocated memory
This architecture guarantees clean termination, even during partial initialization failures—no memory leaks, no zombie locks.
The simulation relies on a strict separation of duties:
- Monitor (Main Thread): Oversees death and completion checks
- Philosophers (Worker Threads): Execute lifecycle routines
This ensures death checks occur independently of philosopher state (eating, sleeping, thinking).
While philosopher threads execute finite_routine or infinite_routine, the main thread becomes a non-blocking Monitor.
-
Continuous Auditing: Loops over:
check_all_philos_deathare_they_full
-
CPU Optimization:
usleep(100)prevents busy-waiting and yields CPU time
-
Atomic Flags:
is_it_dead()uses a mutex-protected boolean (philo_is_dead)- Safely signals all threads to stop immediately upon death
The classic Circular Wait deadlock is eliminated algorithmically in activities.c.
Rule:
A philosopher must always acquire the lower-ID fork first.
// activities.c
first = min(left_id, right_id);
second = max(left_id, right_id);
pthread_mutex_lock(first); // Always lock lower ID
pthread_mutex_lock(second); // Then lock higher IDWhy it works: If philosophers 0–3 lock their left forks, philosopher 4 attempts to lock fork 0 (right) before fork 4 (left). Since fork 0 is already held, philosopher 4 waits—leaving fork 4 free for philosopher 3 to finish, breaking the cycle.
Standard usleep is blocking; a 200ms sleep cannot react to a death at 50ms.
Solution: Sliced Sleeping
- Granularity: Sleep in chunks of 500µs
- Responsiveness: After each chunk, check
is_it_dead()
The simulation can terminate within ~0.5ms after a death instead of waiting for long sleeps to finish.
To avoid a Thundering Herd at t = 0:
- Threads spin-wait on a shared
startflag - Ensures synchronized
start_time
-
Startup: Odd-numbered philosophers wait 2ms
-
Runtime:
- If philosopher count is odd,
philos_sleepadds a 1ms delay - Prevents starvation in tight loops (e.g.
5 800 200 200)
- If philosopher count is odd,
Threads start immediately upon pthread_create, but execution is paused via a synchronization barrier:
static void wait_for_start(t_dinning_info *data, t_philo *philo)
{
while (!data->start)
usleep(100); // Spin-wait with backoff to save CPU
// ...
}The Monitor sets start_time and flips data->start to TRUE, releasing all threads simultaneously.
The simulation uses Relative Time, not absolute system time.
- T = 0: When
data->startbecomes true - Current Time:
Absolute_Time - Start_Time
This guarantees coherent timestamps and accurate death checks across threads.
-
Philosophers loop: Eat → Sleep → Think
-
Atomic checks before every major action:
- Printing
- Eating
- Sleeping
Zombie Prevention:
- Before printing states, verify
is_it_dead() - Monitor exits loops on death or meal completion
pthread_joinensures all threads finish before exit
Compared to Odd/Even or Last-Philosopher-Swap strategies, the Resource Hierarchy (Min/Max) approach is superior:
-
Code Uniformity:
- Every philosopher runs identical logic
- No special cases (
if (id == last))
-
Mathematical Guarantee:
- Circular wait becomes impossible by design
- Deterministic, not heuristic
-
Zero “Magic Delays”:
- No reliance on hardcoded sleeps
- Works regardless of CPU speed or scheduler behavior
If you are tackling the Dining Philosophers problem, here are the critical lessons learned during the development of this project.
Choose one time reference and stick to it.
Mixing system time (wall clock) with simulation time (time since start) is the #1 cause of timing bugs.
Recommended Approach:
Use Relative Time:
current_time = now() - start_time
This guarantees that 0ms is always the exact moment the simulation began, regardless of system delays or thread scheduling.
Avoid a single long sleep like:
usleep(200000); // ❌ BadIf a philosopher sleeps for 200ms in one block, they become blind to death events during that time.
Slice the sleep into small chunks:
while (elapsed < limit)
{
usleep(slice);
check_death();
}- Slices that are too small (e.g.
100µs) cause massive context switching - With many threads, this can saturate the CPU
Sweet Spot:
👉 500µs – 1000µs balances responsiveness and efficiency
Always check is_it_dead():
- Before printing
- After every action (eating / sleeping)
If a philosopher dies while waiting for a fork and you don’t re-check immediately, they might print:
is eating
after they are already dead.
This creates Zombie Philosophers and fails the assignment.
Never use printf without a mutex in a multithreaded program.
❌ Without protection:
100 1 is ea100 2 is sleeping
✅ With protection:
- Use a dedicated
print_lock - Lock → print → unlock
This guarantees clean, readable logs.
Avoid:
-
“Magic numbers”
-
Special-case logic like:
if (id == 100) usleep(500);
These solutions:
- Depend on CPU speed
- Break on different machines
- Are fragile and non-deterministic
Use a Resource Hierarchy approach (e.g. the Min/Max Fork Rule)
Why it’s superior:
- Mathematically prevents deadlocks
- No timing hacks
- Scales to any number of philosophers
- Independent of hardware speed
If a philosopher detects the end of the simulation while holding a fork:
👉 They MUST unlock it before returning
Returning early without unlocking causes:
- Neighbor threads to block forever
pthread_jointo hang- Program never exits → ❌
Always follow this pattern rr something like this. I wanted to save lines so it is not explicity like this (i make up for it with some checks) but you get the main idea:
Lock
Check for death
If dead:
Unlock
Return
Action
Never destroy a mutex that was never initialized.
- Creating 200 forks
- Initialization fails at fork 150
❌ Wrong:
destroy all 200 mutexes✅ Correct:
- Track how many mutexes were successfully created
- Destroy only those
Calling pthread_mutex_destroy on garbage memory leads to:
- Immediate Segmentation Fault
When a philosopher dies:
❌ Do NOT
- Call
exit() - Call
clean_program()from the philosopher thread
Other threads may still be running and will:
- Access freed memory
- Crash with Use-After-Free
-
Flag Set:
philo_is_dead = 1 -
Wait Let all threads detect the flag and exit naturally
-
Join Main thread waits using
pthread_join -
Clean Only then:
- Free memory
- Destroy mutexes
This guarantees a clean, deterministic shutdown with no crashes.
🧠 Final Advice: If your solution depends on timing luck, it’s wrong. If it depends on ordering and invariants, it’s solid.
Use the provided Makefile:
make./philo [number_of_philosophers] [time_to_die] [time_to_eat] [time_to_sleep] [number_of_times_each_philosopher_must_eat (optional)]A philosopher dies due to tight time_to_die.
./philo 4 310 200 100Standard simulation with no deaths.
./philo 5 800 200 200Stops after each philosopher eats 7 times.
./philo 5 800 200 200 7