CSSE7610
Concurrency: Theory and Practice
Week 5
Semaphores
A/Prof Guangdong Bai
[email protected]
https://baigd.github.io/
Process state
Processes are in one of the following of states:
Assume process p has an attribute p.state
p.state ← ready
q.state ← running
Semaphore denition
A semaphore S is a compound data type with two elds
▶ S.V of type non-negative integer
▶ S.L of type set of processes
A semaphore must be initialised with a value k ≥ 0 for S.V and ∅
for S.L
semaphore S ← (k,∅)
There are two atomic statements dened on a semaphore S:
wait(S)
signal(S)
Semaphore denition (cont.)
wait(S)
if S.V > 0
S.V ← S.V − 1
else
S.L ← S.L ∪ {p}
p.state ← blocked
signal(S)
if S.L = ∅
S.V ← S.V + 1
else
let q be an arbitrary element of S.L
S.L ← S.L − {q}
q.state ← ready
Binary semaphore
Initialised with (0,∅) or (1,∅)
wait(S) unchanged
signal(S) changed to:
if S.V = 1
// undened
else if S.L = ∅
S.V ← 1
else // (as before)
let q be an arbitrary element of S.L
S.L ← S.L − {q}
q.state ← ready
A binary semaphore is sometimes called a mutex.
Critical section problem for 2 processes
Critical section with semaphores (two processes)
binary semaphore S ← (1, ∅)
p q
loop forever loop forever
p1: non-critical section q1: non-critical section
p2: wait(S) q2: wait(S)
p3: critical section q3: critical section
p4: signal(S) q4: signal(S)
Critical section problem for 2 processes (cont.)
Critical section with semaphores (two proc., abbrev.)
binary semaphore S ← (1, ∅)
p q
loop forever loop forever
p1: wait(S) q1: wait(S)
p2: signal(S) q2: signal(S)
Semaphore invariants
▶ k is the initial value of the semaphore
▶ #signal(S) is the number of signal statements executed
▶ #wait(S) is the number of wait statements (successfully)
executed
Theorem 1 A semaphore S satises the following invariants:
S.V ≥ 0 (1)
S.V = k + #signal(S) − #wait(S) (2)
Proof: Initially, both are true (since #wait(S) = #signal(S) = 0)
Clearly, (1) is invariant under wait(S) and signal(S)
(2) is also invariant since
▶ wait(S) decrements V when it increments #wait(S)
▶ signal(S) increments both S.V and #signal(S), or increments
both #signal(S) and #wait(S) when it unblocks a process.
Semaphore invariants (cont.)
Theorem 2 Semaphore solution to critical section problem is
correct it ensures mutual exclusion, and freedom from deadlock
and starvation
Proof: Let #CS be the number of processes in their critical
sections
By induction, we know #CS = #wait(S) − #signal(S) is invariant
So, with k = 1, from Theorem 1 we have #CS + S.V = 1
Since S.V ≥ 0, we can deduce #CS ≤ 1 (mutual exclusion)
For deadlock, both processes must be blocked on wait, i.e.
S.V = 0 and #CS = 0. But this contradicts #CS + S.V = 1
(deadlock freedom)
If process p is blocked it is in S.L and S.V = 0. This implies the
other process is in the critical section. When it nishes it executes
signal(S) and p is unblocked (starvation freedom)
The critical section problem for N processes
Critical section with semaphores (N proc.)
binary semaphore S ← (1, ∅)
loop forever
p1: non-critical section
p2: wait(S)
p3: critical section
p4: signal(S)
Critical section with semaphores (N proc., abbrev.)
binary semaphore S ← (1, ∅)
loop forever
p1: wait(S)
p2: signal(S)
Correctness
Proofs for mutual exclusion and deadlock freedom same as for 2
process case
However, starvation can occur:
n Process p Process q Process r S
1 p1: wait(S) q1: wait(S) r1: wait(S) (1, ∅)
2 p2: signal(S) q1: wait(S) r1: wait(S) (0, ∅)
3 p2: signal(S) q1: blocked r1: wait(S) (0, {q})
4 p1: signal(S) q1: blocked r1: blocked (0, {q, r })
5 p1: wait(S) q1: blocked r2: signal(S) (0, {q})
6 p1: blocked q1: blocked r2: signal(S) (0, {p, q})
7 p2: signal(S) q1: blocked r1: wait(S) (0, {q})
Line 7 is the same as line 3, so this behaviour can continue forever.
The problem is that signal(S) unblocks an arbirtary process
Strong semaphores
A strong semaphore replaces the set S.L by a queue
wait(S)
if S.V > 0
S.V ← S.V − 1
else
S.L ← append(S.L, p)
p.state ← blocked
signal(S)
if S.L = ∅
S.V ← S.V + 1
else
q ← head(S.L)
S.L ← tail(S.L)
q.state ← ready
Busy-wait semaphores
No S.L component, so we let S=S.V
wait(S)
await S > 0
S←S−1
signal(S)
S←S+1
n Process p Process q S
1 p1: wait(S) q1: wait(S) 1
2 p2: signal(S) q1: wait(S) 0
3 p2: signal(S) q1: wait(S) 0
4 p1: wait(S) q1: wait(S) 1
Order of execution problems
Semaphores are useful for ordering operations of dierent processes
For example:
Mergesort
integer array A
binary semaphore S1 ← (0, ∅)
binary semaphore S2 ← (0, ∅)
sort1 sort2 merge
p1:sort 1st half of A q1:sort 2nd half of A r1:wait(S1)
p2:signal(S1) q2:signal(S2) r2:wait(S2)
p3: q3: r3:merge halves of A
This is an example of where a binary semaphore is initialised to 0
Producer-consumer problem
Producers create data elements (using a produce method) and
send then to consumers
Consumers receive data elements and executes a consume method
with the data as parameter
To allow for dierent speeds of producers and consumers, data is
stored in a buer
Producer-consumer (innite buer)
innite queue of dataType buer ← empty queue
semaphore notEmpty ← (0, ∅)
producer consumer
dataType d dataType d
loop forever loop forever
p1: d ← produce q1: wait(notEmpty)
p2: append(d, buer) q2: d ← take(buer)
p3: signal(notEmpty) q3: consume(d)
Correctness
Producer-consumer (innite buer, abbreviated)
innite queue of dataType buer ← empty queue
semaphore notEmpty ← (0, ∅)
producer consumer
dataType d dataType d
loop forever loop forever
p1: append(d, buer) q1: wait(notEmpty)
p2: signal(notEmpty) q2: d ← take(buer)
Split semaphores
Producer-consumer (nite buer, semaphores)
nite queue of dataType buer ← empty queue
semaphore notEmpty ← (0, ∅)
semaphore notFull ← (N, ∅)
producer consumer
dataType d dataType d
loop forever loop forever
p1: d ← produce q1: wait(notEmpty)
p2: wait(notFull) q2: d ← take(buer)
p3: append(d, buer) q3: signal(notFull)
p4: signal(notEmpty) q4: consume(d)
This algorithm uses a split semaphore, i.e. two or more
semaphores whose sum is at most equal to a xed number N
▶ nonEmpty + notFull ≤ N is invariant above
Dining philosophers
loop forever
think
preprotocol
eat
postprotocol
A philosopher needs to hold 2 forks in order to eat.
Dining philosophers (rst attempt)
Dining philosophers (rst attempt)
semaphore array [0..4] fork ← [1,1,1,1,1]
loop forever
p1: think
p2: wait(fork[i])
p3: wait(fork[i+1] mod 5)
p4: eat
p5: signal(fork[i])
p6: signal(fork[i+1] mod 5)
Theorem 3 No fork is held by 2 philosophers
Proof: Follows from mutual exclusion property of semaphores
(Theorem 2).
But deadlocks when all philosophers pick up their left forks.
Dining philosophers (second attempt)
Introduce a room semaphore allowing only 4 philosophers in the
room at a time
Dining philosophers (second attempt)
semaphore array [0..4] fork ← [1,1,1,1,1]
semaphore room ← 4
loop forever
p1: think
p2: wait(room)
p3: wait(fork[i])
p4: wait(fork[i+1] mod 5)
p5: eat
p6: signal(fork[i])
p7: signal(fork[i+1] mod 5)
p8: signal(room)
Mutual exclusion still holds, does starvation freedom hold?
Starvation freedom
Theorem 4 The second attempt is free from starvation.
Proof: Assume the room semaphore is a strong semaphore.
Philosopher i starves when they are blocked on a semaphore:
▶ blocked on left fork philosopher to left is holding their right
fork and by assumption of progress on the critical section will
eventually release it
▶ blocked on right fork philosopher to right is holding their
left fork and will either eventually release it, or is blocked on
their right fork. In the latter case, all philosophers must be
blocked on their right forks. But the room semaphore prevents
this case.
▶ blocked on room this is only possible if its value is 0
indenitely. However, the previous cases show that
philosophers are not blocked, so one will leave eventually.
Dining philosophers (third attempt)
Introduce asymmetry one philosopher who starts with the right
fork.
Dining philosophers (third attempt)
semaphore array [0..4] fork ← [1,1,1,1,1]
philosopher 4
loop forever
p1: think
p2: wait(fork[0])
p3: wait(fork[4])
p4: eat
p5: signal(fork[0])
p6: signal(fork[4])
The proof of freedom from starvation is similar to the second
attempt.
Barz's simulation of general semaphore
Barz's algorithm for simulating general semaphores
binary semaphore S ← 1
binary semaphore gate ← 1
integer count ← k
loop forever
non-critical section
p1: wait(gate)
p2: wait(S) // Simulated wait
p3: count ← count − 1
p4: if count > 0 then
p5: signal(gate)
p6: signal(S)
critical section
p7: wait(S) // Simulated signal
p8: count ← count + 1
p9: if count = 1 then
p10: signal(gate)
p11: signal(S)
Busy-wait semaphores in Promela
▶ Busy-wait semaphore:
inline wait ( s ) {
atomic { s > 0 ; s= = }
}
inline s i g n a l ( s ) { s++ }
byte sem = 1 ;
active [ 2 ] proctype P ( ) {
do : :
w a i t ( sem ) ;
printf ( "%d i n CS\n" , _pid ) ;
s i g n a l ( sem )
od
}
Proving Barz's algorithm in Spin
#define NPROCS 3
#define K 2
byte g a t e = 1 , s =1; int c o u n t = K;
active [ NPROCS ] proctype P ( ) {
do : : w a i t ( g a t e ) ;
wait ( s ) ;
count ==;
if : : c o u n t > 0 => s i g n a l ( g a t e ) ;
else
::
fi ;
signal (s );
printf ( "%d i n CS\n" , _pid ) ;
wait ( s ) ;
c o u n t ++;
if : : c o u n t == 1 => s i g n a l ( g a t e ) ;
:: else
fi ;
signal (s );
od
}
Weak semaphores in Promela
▶ Weak semaphore:
typedef Semaphore {
byte count ;
bool b l o c k e d [ NPROCS ] ;
};
inline initSem (S , n) {
S . count = n
}
inline w a i t ( S ) {
atomic {
if
: : S . c o u n t >= 1 => S . count = =
: : else => S . b l o c k e d [ _pid = 1] = true ;
! S . b l o c k e d [ _pid = 1]
fi
}
}
Weak semaphores in Promela (cont.)
For 3 processes:
inline s i g n a l ( S ) {
atomic {
if
: : S . b l o c k e d [ 0 ] => S . b l o c k e d [ 0 ] = false
: : S . b l o c k e d [ 1 ] => S . b l o c k e d [ 1 ] = false
: : S . b l o c k e d [ 2 ] => S . b l o c k e d [ 2 ] = false
:: else => S . c o u n t++
fi
}
}
For an arbitrary number of processes, we need to add variables i
(for looping through blocked) and choice for recording the choice of
process to unblock
A nondeterministic choice is made using the nondeterminism in
Promela if statements
Weak semaphores in Promela (cont.)
inline s i g n a l ( S ) {
atomic {
S. i = 0;
S . choice = 255;
do : : ( S . i == NPROCS) => break
: : ( S . i < NPROCS) && ! S . b l o c k e d [ S . i ] => S . i++
:: else =>
if : : ( S . c h o i c e == 2 5 5 ) => S . c h o i c e = S . i
: : ( S . c h o i c e != 2 5 5 ) => S . c h o i c e = S . i
: : ( S . c h o i c e != 2 5 5 ) =>
fi ;
S . i++
od ;
if : : S . c h o i c e == 255 => S . c o u n t++
:: else false
=> S . b l o c k e d [ S . c h o i c e ] =
fi
}
}