Philosopher
"The philosophers keep thinking, the forks keep waiting β and the only true enemy is time."
- The Problem
- What is a Thread?
- Thread vs Process
- Thread Control Block (TCB)
- Thread Lifecycle & Creation
- Concurrency vs Parallelism
- Race Conditions
- Mutex β The Gatekeeper
- Semaphore β The Counter Guard
- POSIX & pthreads
- pthread_join β Waiting Gracefully
- Project Structure
- Usage & Compilation
- Mandatory vs Bonus
A classic concurrency problem posed by Edsger W. Dijkstra in 1965.
[Philosopher 1]
π€ π΄
π΄ π΄
[Philo 5] πππ [Philo 2]
π΄ π΄
π΄ π€
[Philosopher 4]
π€
[Philosopher 3]
Scenario:
Nphilosophers sit around a circular table with a shared bowl of spaghetti.- Each philosopher alternates between thinking, eating, and sleeping.
- Between each pair of philosophers lies a single fork.
- To eat, a philosopher needs two forks (left and right).
- A philosopher dies if they go too long without eating.
The challenge: Coordinate fork access to prevent:
- π Starvation β a philosopher never gets to eat.
- π Deadlock β all philosophers hold one fork and wait forever.
- π Race conditions β unpredictable behavior from unsynchronized access.
A thread is the smallest unit of execution within a program.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PROCESS β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SHARED MEMORY SPACE β β
β β β’ Code Segment (text) β β
β β β’ Global Variables β β
β β β’ Heap Memory β β
β β β’ File Descriptors β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Thread 1 β β Thread 2 β β Thread 3 β β
β β (main) β β (philo 1) β β (philo 2) β β
β β own stack β β own stack β β own stack β β
β β own PC β β own PC β β own PC β β
β β own regs β β own regs β β own regs β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Property | Thread | Process |
|---|---|---|
| Memory Space | Shared with parent process | Isolated, private |
| Stack | Private per thread | Private per process |
| Program Counter | Private (own instruction pointer) | Private |
| Creation Cost | Low (fast) | High (slow) |
| Communication | Direct (shared memory) | IPC (pipes, sockets, etc.) |
| Context Switch | Fast | Slow |
π A single thread cannot be executed by multiple cores simultaneously.
π Each thread can be executed by a single CPU core.
Context switching between threads is significantly faster than between processes due to three key reasons:
PROCESS context switch:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Save ALL registers + memory maps + page tablesβ
β + file descriptors + signal handlers ... β
β βββββββββββ HEAVY OVERHEAD βββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
THREAD context switch:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Save: stack pointer + program counter β
β + registers β
β βββββββββββ LIGHT OVERHEAD βββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
- Shared Memory Space β No need to swap page tables or memory maps.
- Lower Overhead β Thread state (TCB) is a fraction of process state (PCB).
- Reduced State Info β Only stack, PC, and registers need saving/restoring.
Every thread is represented in the OS by a Thread Control Block (TCB) β a data structure storing all thread-specific state.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β THREAD CONTROL BLOCK β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Thread ID (TID) β Unique identifier β
β 2. Program Counter (PC) β Next instruction β
β 3. CPU Registers β Current reg state β
β 4. Stack Pointer β Top of the stack β
β 5. State β Running/Ready/... β
β 6. Priority β Scheduling weight β
β 7. Thread-Local Storage β Private variables β
β 8. Resource References β Memory, file hdls β
β 9. Parent PCB pointer β Owning process β
β 10. Scheduling Info β CPU time, affinityβ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
pthread_create()
β
βΌ
ββββββββββββ
β READY βββββββββββββββββββββββββ
ββββββ¬ββββββ β
β scheduler picks thread β time-slice expires /
βΌ β preempted
ββββββββββββ β
β RUNNING βββββββββββββββββββββββββ
ββββββ¬ββββββ
β waiting for resource / I/O
βΌ
ββββββββββββ
β BLOCKED β
ββββββ¬ββββββ
β resource available
ββββββββββββΊ READY (again)
β
ββββββΌββββββ
βTERMINATEDβ β pthread_exit() / return from thread fn
ββββββββββββ
Program starts
β
βΌ
OS creates Process Control Block (PCB)
β
ββββ Initial thread = main thread
β
βΌ
pthread_create() called
β
βΌ
ββββββββββββββββββββββββββββββ
β OS allocates new TCB β
β OS initializes TCB with: β
β β’ entry function address β
β β’ function arguments β
β β’ initial priority β
β β’ stack allocation β
βββββββββββββββ¬βββββββββββββββ
β
βΌ
Thread state = READY
β
βΌ
Scheduler picks thread β CPU loads TCB
β
βΌ
Thread RUNNING π
// Thread entry function
void *routine(void *arg)
{
t_philo *philo = (t_philo *)arg;
// philosopher logic here
return (NULL);
}
// Creating a thread
pthread_create(&philo->thread, // thread identifier
NULL, // default attributes
routine, // entry point function
&philo); // argument passed to functionvoid create_threads(t_main *main, int philo_nbr)
{
int i = 0;
main->start_time = get_time_in_ms();
while (i < philo_nbr)
{
main->philos[i].main = main;
main->philos[i].last_meal = main->start_time;
pthread_create(&main->philos[i].thread, NULL,
routine, &main->philos[i]);
i++;
}
}SEQUENTIAL:
ββββββββββββββββββββββββββββββββββββββββββββββΊ time
[Task A ββββββββ][Task B ββββββββ][Task C ββββββββ]
CONCURRENT (1 core):
ββββββββββββββββββββββββββββββββββββββββββββββΊ time
[Aββ][Bββ][Aββ][Cββ][Bββ][Aββ][Cββ][Bββ]
β rapid context switches create the ILLUSION of simultaneity
PARALLEL (multiple cores):
Core 1: [Task A ββββββββββββββββββββββββ]
Core 2: [Task B ββββββββββββββββββββββββ]
Core 3: [Task C ββββββββββββββββββββββββ]
ββββββββββββββββββββββββββββββΊ time
On a single-core CPU, only one thread truly executes at a time. Context switching happens so fast (in nanoseconds) that it appears as if all threads run simultaneously β but it's an illusion created because:
- Human perception of time is too slow to notice the rapid switches.
- The OS context-switches between threads thousands of times per second.
π A core is considered by the computer as its own CPU.
| Model | Description | Example |
|---|---|---|
| Sequential | Tasks run one after another | Reading a file then writing it |
| Concurrent | Tasks progress independently, may interleave | Web server handling requests |
| Parallel | Tasks execute truly simultaneously on multiple cores | Splitting a big computation |
A race condition arises when two or more threads access and modify a shared resource at the same time, and the outcome depends on the unpredictable timing of these accesses.
Thread 1 reads: counter = 5
Thread 2 reads: counter = 5 β reads BEFORE Thread 1 writes back!
Thread 1 writes: counter = 6
Thread 2 writes: counter = 6 β WRONG! Should be 7
Fork = shared resource
β
βββ Philosopher 1 grabs fork (reads state: available)
β
βββ Context switch happens here β danger zone!
β
βββ Philosopher 2 ALSO grabs the same fork (also reads: available)
β Both think they have exclusive access β DATA RACE!
Solution: Use synchronization primitives β mutexes or semaphores.
A mutex (Mutually Exclusive flag) is the security guard of concurrent programming. It ensures that only one thread at a time can access a shared resource.
βββββββββββββββββββββββββββββββββββββββββββββββ
β MUTEX β
β β
β π LOCKED β only one thread inside β
β π UNLOCKED β any thread may enter β
β β
β Components: β
β β’ Lock State (locked / unlocked) β
β β’ Owner Thread (which thread holds it) β
β β’ Wait Queue (threads waiting to enter)β
β β’ Condition Vars (signaling between threads)β
βββββββββββββββββββββββββββββββββββββββββββββββ
Thread 1 Thread 2
ββββββββ ββββββββ
pthread_mutex_lock(&fork) pthread_mutex_lock(&fork)
β (acquires lock π) β (BLOCKS β mutex is locked)
[critical section] [waiting...]
[use shared resource] [waiting...]
pthread_mutex_unlock(&fork) β (wakes up, acquires lock π)
[critical section]
pthread_mutex_unlock(&fork)
// Protect a shared message print
pthread_mutex_lock(&philo->main->message_lock);
printf("%d %d is eating\n", time, philo->id);
pthread_mutex_unlock(&philo->main->message_lock);A semaphore is a signaling mechanism represented by an integer value shared between threads. Its purpose is to protect a critical section from being entered by more threads than allowed.
Semaphore value = N β up to N threads may enter simultaneously
sem_wait(sem) ββ decrements value; blocks if value == 0
sem_post(sem) ββ increments value; wakes a waiting thread
Semaphore (value = 2):
Thread 1 β sem_wait() β value = 1 β enters
Thread 2 β sem_wait() β value = 0 β enters
Thread 3 β sem_wait() β value = -1 β BLOCKED (waits)
Thread 1 β sem_post() β value = 0 β Thread 3 unblocked β enters
| Feature | Mutex | Semaphore |
|---|---|---|
| Value | Binary (locked/unlocked) | Integer (0 to N) |
| Ownership | Owned by locking thread | No ownership concept |
| Use Case | Mutual exclusion | Resource counting / signaling |
| Signals | No | Yes (can signal other threads) |
Used in
philo_bonuswhere processes replace threads, requiring semaphores instead of mutexes.
POSIX (Portable Operating System Interface) is a set of standards defining how software should interact with the OS, ensuring portability across Unix-like systems (Linux, macOS, BSDβ¦).
POSIX defines:
β’ Standard CLI tools: cd, ls, grep, catβ¦
β’ Standard system calls: open(), read(), write(), close()β¦
β’ Standard thread API: pthread_create(), pthread_join(), pthread_mutex_*β¦
| Goal | Description |
|---|---|
| Portability | Write once, run on any POSIX-compliant OS without modification |
| Interoperability | Different systems communicate using standardized interfaces |
| Consistency | Predictable behavior reduces bugs and development time |
| Ecosystem | Common interface encourages broader software development |
pthread_create() // Create a new thread
pthread_join() // Wait for a thread to finish
pthread_mutex_init() // Initialize a mutex
pthread_mutex_lock() // Lock a mutex (blocking)
pthread_mutex_unlock()// Release a mutex
pthread_mutex_destroy()// Clean up a mutexpthread_join() makes the calling thread wait until the specified thread terminates.
Without pthread_join(): With pthread_join():
βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ
main() main()
pthread_create(philo1) pthread_create(philo1)
pthread_create(philo2) pthread_create(philo2)
main exits immediately β pthread_join(philo1) βββ
philos become zombies β οΈ pthread_join(philo2) βββ€
main exits cleanly β
β
β
waits until all threads end β
- Thread Creation still works β
pthread_create()creates threads regardless. - Main exits early β Without joining, main may terminate before philosophers finish.
- Zombie Threads β Threads still running after main exits β resource leaks, undefined behavior.
// After creating all philosopher threads:
void *join_threads(t_philo *philos, int philo_nbr)
{
int i = 0;
while (i < philo_nbr)
{
pthread_join(philos[i].thread, NULL);
i++;
}
return (NULL);
}Philosopher/
βββ philo/ β Mandatory (threads + mutexes)
β βββ main.c β Entry point, thread creation, struct init
β βββ philo.c β Philosopher routine (eat/sleep/think)
β βββ simulation.c β Monitor thread, death detection
β βββ philo_utils.c β Time utilities, sleep, reader/writer
β βββ parse.c β Argument validation
β βββ ft_atoi.c β String to integer conversion
β βββ philo.h β Structs, defines, prototypes
β
βββ philo_bonus/ β Bonus (processes + semaphores)
βββ main.c β Entry point, process creation
βββ simulation.c β Philosopher process logic
βββ bonus_utils.c β Utility functions
βββ clean.c β Resource cleanup
βββ philo_bonus.h β Structs, defines, prototypes
typedef struct main_struct
{
pthread_mutex_t message_lock; // Protects console output
pthread_mutex_t dead_flag; // Protects death flag
pthread_mutex_t meal_flag; // Protects meal count
pthread_mutex_t *forks; // One mutex per fork
pthread_t monitor; // Monitor thread
t_philo *philos; // Array of philosophers
int start_time; // Simulation start (ms)
int time_eat; // ms to eat
int time_die; // ms before dying of hunger
int time_sleep; // ms to sleep
int philo_nbr; // Number of philosophers
int meals; // Optional meal limit
int dead_sign; // 0 if someone died
} t_main;
typedef struct philo
{
pthread_t thread; // This philosopher's thread
pthread_mutex_t *right_fork; // Fork on the right
pthread_mutex_t *left_fork; // Fork on the left
t_main *main; // Pointer to shared state
int id; // Philosopher number (1-based)
int last_meal; // Timestamp of last meal
int meals_nbr; // How many meals eaten
} t_philo;# Mandatory version (threads + mutexes)
cd philo
make
# Bonus version (processes + semaphores)
cd philo_bonus
make./philo <number_of_philosophers> <time_to_die> <time_to_eat> <time_to_sleep> [number_of_times_each_philosopher_must_eat]| Argument | Description |
|---|---|
number_of_philosophers |
Also the number of forks on the table |
time_to_die (ms) |
A philosopher dies if they haven't eaten in this time |
time_to_eat (ms) |
Time it takes to eat (holding two forks) |
time_to_sleep (ms) |
Time spent sleeping after eating |
number_of_times_each_philosopher_must_eat |
Optional: simulation stops when all reach this count |
# 5 philosophers, die at 800ms, eat in 200ms, sleep 200ms
./philo 5 800 200 200
# Same but stop after each philosopher eats 7 times
./philo 5 800 200 200 7
# Edge case: 1 philosopher (should die β only 1 fork)
./philo 1 800 200 200timestamp_in_ms philosopher_id action
Example:
0 1 has taken a fork
0 1 has taken a fork
10 1 is eating
210 1 is sleeping
410 1 is thinking
800 2 died
Mandatory (philo/) |
Bonus (philo_bonus/) |
|
|---|---|---|
| Concurrency Model | Threads (pthread) |
Processes (fork) |
| Synchronization | Mutexes (pthread_mutex_t) |
Semaphores (sem_t) |
| Memory Sharing | Shared (same process) | Separate per process |
| IPC Mechanism | Shared variables (mutex-guarded) | Semaphores (POSIX named sems) |
| Death Detection | Monitor thread | Monitor process / signal |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Threads share memory β fast communication, but dangerous. β
β Mutexes prevent race conditions β only one thread at a time. β
β Semaphores control access to N resources simultaneously. β
β Context switching is fast (ns) β concurrency feels like magic. β
β pthread_join ensures clean shutdown β never leave zombies. β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Made with π§ by Samia-Hb β 1337 school