Semaphores
The dining philosophers problem
A bunch (
N
) of philosophers who spend their time thinking and eatingA philosopher:
- Love Asian food! (chopsticks not fork and knife)
- Only
N
chopsticks on the table (funding cuts) - Cannot eat with only one chopstick
- Can only take the chopsticks on the side
Write a program which simulates the behavior of the philosophers
What is the purpose?
Classical problem illustrating
- Mutex: only one philosopher at a time may have a chopstick
- Conditional synchronisation: may eat only when has two chopsticks
- Deadlock
- Livelock (difference with deadlock?)
- Starvation
First attempt
Let's start from the top-level (Code here)
public Table1(int m) { MAX = m ; // How many are sit chops = new Semaphore[MAX] ; // Chopsticks phil = new Phil1[MAX] ; // Philosophers for (int i = 0 ; i < MAX ; i++) chops[i] = new Semaphore(1,true) ; for (int i = 0 ; i < MAX ; i++) phil[i] = new Phil1(i,MAX,chops) ; } public void run () { for (int i = 0 ; i < MAX ; i++) phil[i].start() ; }
We model chopsticks as semaphores
- Taken, the semaphores is
0
- Available, the semaphore is
1
- Taken, the semaphores is
-
public void run () { int left = i ; int right = (i+1)%MAX; while (true) { System.out.println("Philisopher "+i+" is thinking"); try { Thread.sleep(500+(int)(Math.random()*1000)) ;} catch (InterruptedException e) {} ; try { chops[left].acquire() ; chops[right].acquire() ; } catch (InterruptedException e) {} System.out.println("Philisopher "+i+" is eating"); try { Thread.sleep(500+(int)(Math.random()*1000)) ;} catch (InterruptedException e) {} ; chops[left].release() ; chops[right].release() ; } }
Does it work?
- Deadlock/Starvation? Deadlock is possible
First attempt (Deadlock)
If all manage to pick up their left-hand fork at about the same time then a deadlock occurs
chops[left].acquire() ; try { Thread.sleep(2000) ;} catch (InterruptedException e) {} ; chops[right].acquire() ;
In general deadlock occurs because there is a circular waiting
Solution 1 : Break the symmetry
Prevention by not allowing the circular waiting to arise.
- One of the philosophers must break the cycle
We change the code for philosophers and tables
if (i == 0) { try { forks[right].acquire() ; forks[left].acquire() ; } catch (InterruptedException e) {} } else { try { forks[left].acquire() ; forks[right].acquire() ; } catch (InterruptedException e) {} }
Deadlock? Deadlock is avoided.
- Starvation? Starvation is avoided with fair semaphores.
Solution 2 : Limit the usage of resources
If at most N-1 philosophers are eating then at least one will always have two forks.
Use a general semaphore to represent N-1 available chairs (
chairs
)try { chairs.acquire() ; chops[left].acquire() ; chops[right].acquire() ; } catch (InterruptedException e) {}
chops[left].release() ; chops[right].release() ; chairs.release() ;
- The code for philosophers and tables
- Deadlock? Deadlock is avoided.
- Starvation? Starvation is avoided with fair semaphores.
Starvation
Starvation with a fair scheduler?
- Depends on the implementation of semaphores!
- Blocked queue (FIFO) guarantees fairness
- Other “queue” policy might not
Java?
- Default constructor
Semaphore
: no Semaphore(int permits, boolean fair)
- Why would you ever want to use a non-fair semaphore in Java? Because it might be faster.
- Default constructor
Preventing deadlocks: Order of resources
One simple method for guaranteeing deadlock freedom when using locks for mutex
Fix a (linear) order for resources. Only allowed to possess multiple resources if they are acquired in that order
- Solution 1 for the philosophers' problem
Preventing deadlock: Break the chain
Identify the “circular waiting chains”
Don’t allow enough processes to “fill” the chain
- Solution 2 for the philosophers' problem
Producer-Consumer
Producer-consumer relationships between processes are a very common pattern
Problem: how do we allow for different speeds of production vs consumption?
Unbounded buffers
Infinite bar!
Producer can work freely
Consumer must wait for producer
...
Bounded buffers
Real life!
Producer must wait if buffer is full
Consumer must wait if buffer is empty
One-slot buffer
Multiple producer threads
Multiple consumer threads
Two reasons to block:
Producers wait for an (the) empty slot to be available
Consumers wait for a filled slot to be available
Any ideas?
public void put(E e) { < await empty > 0: empty-- ; buf = e > full++ ; } public E get() throws InterruptedException { E result ; < await full == 1: full-- ; result = buf > empty++ ; return result; }
How do we implement < B : S >
- We use semaphores
Initialization
- One empty slot
empty = new Semaphore (1,true) ;
- Zero full slots
full = new Semaphore (0,true) ;
Invariant
empty + full <= 1
-
public void put(E e) throws InterruptedException { empty.acquire(); buf = e; full.release(); } public E get() throws InterruptedException { E result ; full.acquire(); result = buf ; empty.release(); return result; }
A producer and consumer with different speeds
- Producer
for (int i = 0 ; i < P ; i++) { try { Thread.sleep(Speed) ;} catch (InterruptedException e) {} ; System.out.println("Produced"+i); try { buf.put(i) ; } catch (InterruptedException e) {} }
- Consumerfor (int i = 0 ; i < C ; i++) { try { Thread.sleep(Speed) ;} catch (InterruptedException e) {} ; System.out.print("Consumed "); try { d = buf.get() ; } catch (InterruptedException e) {} System.out.println(d); }
Here, you can see the code that puts all the pieces together
General N-slot buffer
Producer(s) add to the back (tail) of the buffer
Consumer(s) take from the front (head) of the buffer
Count the number of free/full slots as before
Assume that the buffer has size
S
How should I initialize the
empty
andfull
semaphores?empty = new Semaphore (S,true) ; full = new Semaphore (0,true) ;
Invariant
empty + full <= S
Single producer
public void put(E e) throws InterruptedException { empty.acquire(); buf[front] = e; front = (front+1)%S; full.release(); }
Single consumer
public E take() throws InterruptedException { full.acquire(); E result = buf[rear]; rear = (rear+1)%S; empty.release(); return result; }
Does it work?
front
is shared and updated among producersrear
is shared and updated among consumers
Share-update problem again! How did we solve it?
Mutex!
How many?
mutexP
: a mutex for producerspublic void put(E e) throws InterruptedException { empty.acquire(); mutexP.acquire(); buf[front] = e; front = (front+1)%S; mutexP.release(); full.release(); }
Can we interchange lines 2 and 3?
Can we interchange lines 6 and 7?
mutexC
: a mutex for consumerspublic E take() throws InterruptedException { full.acquire(); mutexC.acquire(); E result = buf[rear]; rear = (rear+1)%S; mutexC.release(); empty.release(); return result; }
Can we interchange lines 2 and 3?
Can we interchange lines 6 and 7?
Preparing for blocking
A call to
s.acquire()
may involve blocking for a long timeA process might need to take precautions before waiting (e.g. set controlled device in safe state)
Problem: Precautions unnecessarily taken also when no waiting occurs
take_precautions() ; s.acquire() ;
- If there is no need for blocking,
take_precautions()
has been called for no reason (it might be expensive)
Another operation on semaphores
boolean tryAcquire()
A non-blocking operation acquiring the semaphore (and returns true) if it is possible at time of invocation. Otherwise, returns false (without acquiring the semaphore).
Ignores fairness setting!
Readers-writers problem
Another classic synchronisation problem
Two kinds of threads share access to a “database”
- Readers examine the contents
- Multiple readers allowed concurrently
- Writers examine and modify
- A writer must have mutex
Invariant
- ( nr==0 ∨ nw==0 ) ∧ nw <= 1
Any ideas?
< await (nw==0): nr++; > // read database < nr-- ; >
< await (nr==0 && nw==0): nw++; > // write database < nw--; >
Generally speaking, how to implement a general
await
statement using semaphores?Technique "passing the baton"
Not easy to understand (read it from the book!)
Summary
How to avoid deadlock
- Philosophers' problem
Synchronization patterns
Producers-Consumers
Readers-Writers (shortly, more comes later)
Semaphores: extra operation
tryAcquire
Semaphores pros
Simple, efficient, expressive
Passing the baton – any await statement
Semaphores cons
Low level, unstructured
- Omit a
release
: deadlock - Omit a
acquire
: failure of mutex
- Omit a
Synchronisation code not linked to the data
Synchronisation code can be accessed anywhere, but good programming style helps!