Examples of unsynchronized access that may lead to non-deterministic outcome (based on order of access/modification, aka race conditions.)
Critical section: does critical work accessing shared modifiable resources. At any point only one program can execute in that critical section.
TestAndSet( Register, MemLocation )
*MemLoc
into $Reg
*MemLoc
$Reg
contentsvoid EnterCS (int *lock) {
/* i.e. while the CS process has set *MemLoc to 1,
* other processes trying to set access is stuck in while loop
* as TestAndSet returns 1. */
while (TestAndSet( lock ) == 1);
}
void ExitCS (int *lock) {
*lock = 0;
}
Problems: busy waiting wastes processing power.
Solution: variants of the instructions.
Variants: Compare and Exchange, Atomic Swap, Load Link/Store Conditional
Turn
array. While Turn != P.turn
, busy wait.Want
array. Busy wait while someone else wants the turn.Turn
and Want
: Peterson’s algorithmTurn
cannot be 1 and 0 at once, so only one of the while
loops ends at firstTurn
now indicates which process’s turn is it.We assume that line 2 (writing to Turn
in each process) is atomic.
Generalized sync mechanism.
Contains one int
representing capacity,
wait()
blocks if capacity $\leq 0$, otherwise decrement.
signal()
increments capacity, wakes up 1 sleeping process.
Invariant for semaphore $s$:
\[s_\text{curr} = s_\text{init} + \# \text{signals executed}(s) - \# \text{waits completed}(s)\]Binary semaphores with capacity of 1.
Number of processes in critical section: $N_{CS} = \text{waits completed}(s) - \text{signals executed}(s)$
\[s_\text{curr} = \textcolor{blue}{1 - N_{CS}} \geq 0 \Rightarrow \textcolor{red}{N_{CS} \leq 1}\]No deadlock where $N_{CS} = s_\text{curr} = 0$, granted correct usage.
No starvation if fair scheduling.
How to wait for another thread to do something specific?
wait(condition)
waits for condition
to become true i.e. signaledsignal(condition)
wakes up one sleeping processbroadcast(condition)
wakes up allNote: after a thread is woken up, the condition may not hold anymore.
A collection of procedures manipulating a shared data structure, one lock must be held when accessing the shared data.
From The Little Book of Semaphores.
# Thread A
semA = 0
statement a1
signal(semA)
wait(semB)
statement a2
# Thread B
semB = 0
statement b1
signal(semB)
wait(semA)
statement b2
rendezvous // all n-1 threads block until nth thread reaches rendezvous
critical point
$n$ threads. No thread executes critical point until all have reached rendezvous.
wait(mutex)
waitingThreads += 1
if (waitingThreads == n) signal(barrier) -- (1)
signal(mutex)
wait(barrier)
signal(barrier) -- (2)
critical point
(1) Triggers first waiting thread to pass through. Now value of barrier is $-(n-2)$.
(2) You must signal(barrier)
after the each thread is done waiting for the barrier, otherwise the next in queue will not be able to execute (deadlock). This pattern of wait(b), signal(b)
is also called a turnstile.
How to lock the barrier again after all the threads have passed?
wait(mutex)
waitingThreads += 1
if (waitingThreads == n)
wait(t2) -- wait for nth thread to unlock 2nd turnstile
signal(t1) -- signals that the 1st turnstile is unlocked
signal(mutex)
wait(t1)
signal(t1)
critical point
wait(mutex)
waitingThreads -= 1
if (waitingThreads == 0)
wait(t1) -- wait for nth thread to unlock 1st turnstile
signal(t2) -- signals that the 2nd turnstile is unlocked
signal(mutex)
wait(t2)
signal(t2)
Two-phase barrier. Note we cannot solve this problem with a single turnstile, e.g.
If the $n$th thread passes through the CP, decrements count without interruption and then returns to the rendezvous (in a loop), and increments count again, it restores the count to $n$ and thus gets ahead of the other threads.
Shared bounded buffer of size $K$. Constraints are:
# Producers: notFull initialized to K
wait(notFull) -- (1)
wait(mutex)
buffer[in] = item
in = (in+1) % K
signal(mutex)
signal(notEmpty)
# Consumers: notEmpty initialized to 0
wait(notEmpty) -- (1)
wait(mutex)
item = buffer[out]
out = (out-1) % K
signal(mutex)
signal(notFull)
use item
(1) Note this is a rendezvous. Removes the need to check the item count in that buffer.
Example of categorical mutual exclusion. Readers don’t exclude other threads, but writers do. Constraints:
# Writers: notEmpty initialized to 1
wait(openToWriters)
/* prioritization
* wait(noWriters)
* write
* signal(noWriters)
*/
signal(openToWriters)
# Readers
wait(mutex)
readerCount += 1
if (readerCount == 1)
wait(openToWriters) -- (1)
signal(mutex)
read
/* prioritization:
* signal(noWriters)
*/
wait(mutex)
readerCount -= 1
if (readerCount == 0) signal(openToWriters)
return(mutex)
signal
(1) Prior to this step, if readerCount == 1
then wait until all writers gone (openToWriters
)
openToWriters
is the same immediately before and immediately after writeropenToWriters
is only increased by readerCount == 0
.openToWriters
lock closes it off to writers (since it a mutex).Drawbacks: starvation of writer possible.
Solution: Priority to writers. (see /* prioritization */
snippets)
Image | Specification |
---|---|
![]() |
- 5 philosophers seated around a table. - 5 single chopsticks between them - To eat, one must acquire his left and right chopstick - Deadlock-free and starve-free to allow philosophers to freely eat |
Simple informal argument (for “One right-hander”. Let R be the right hander):
Take chopsticks | Safe to eat | Put chopsticks |
---|---|---|
![]() |
![]() |
![]() |