Title: Semaphores
1Semaphores
UNIVERSITY of WISCONSIN-MADISONComputer Sciences
Department
CS 537Introduction to Operating Systems
Andrea C. Arpaci-DusseauRemzi H.
Arpaci-Dusseau Haryadi S. Gunawi
- Questions answered in this lecture
- Why are semaphores necessary?
- How are semaphores used for mutual exclusion?
- How are semaphores used for scheduling
constraints? - Examples Join and Producer/Consumer and
Readers/Writers
2Motivation for Semaphores
- So far Locks only provide mutual exclusion
- Ensure only one thread is in critical section at
a time - L?lock can only be true/false (hence, some
limitations exist) - Cannot solve all real-world problems
- May want more Place ordering on scheduling of
threads - Examples
- Producer/Consumer
- Producer Creates a resource (data)
- Consumer Uses a resource (data)
- ps grep gcc wc
- thread_join()
- Dont want producers and consumers to operate in
lock step - Place a fixed-size buffer between producers and
consumers - Synchronize accesses to buffer
- Producer waits if buffer full consumer waits if
buffer empty - No point to run producer threads if buffer is
full - No point to run consumer threads if buffer is
empty
3Semaphores
- Semaphores Introduced by Dijkstra in 1960s
- Semaphores have two purposes
- Mutex
- Ensure threads dont access critical section at
same time - Scheduling constraints
- Ensure threads execute in specific order
4Semaphore Operations
- Allocate and Initialize
- Semaphore initially contains a non-negative
integer value - User cannot read or write value directly after
initialization - There are only 3 operations init, wait and
signal - Sem_t sem
- Int sem_init(sem, is_shared, init_value)
- Wait or Test
- P() for test in Dutch (proberen)
- Waits until value of sem is gt 0, then decrements
sem value - Int sem_wait(sem)
- Signal or Increment or Post
- V() for increment in Dutch (verhogen)
- Increments value of semaphore
- Int sem_post(sem)
5Semaphore Implementation
- typedef struct
- int value
- queue tlist
- semaphore
- wait (semaphore S) // Must be executed
atomically - S-gtvalue--
- if (S-gtvalue lt 0)
- add this process to S-gttlist
- block()
-
-
- signal (semaphore S) // Must be executed
atomically - S-gtvalue
- if (S-gtvalue lt 0)
- remove thread t from S-gttlist
- wakeup(t)
-
6Semaphore Example
- What happens if sem is initialized to 2?
- Scenario Three threads call wait(sem)
- Two threads can proceed
- One thread will be blocked (current S.value -1)
- Observations
- Initial sem value is positive ? Number of threads
that can be in c.s. at same time - Current sem value is negative ? Number of waiters
on queue currently
7Ex1 Mutual Exclusion with Semaphores
- Previous example with lock L
- lock(L)
- balance amount
- unlock(L)
- Example with semaphore S
- wait(S)
- balance amount
- signal(S)
- To what value should S be initialized? 1
- Whenever you initialize a semaphore value to 1,
it will work like a lock (I.e. Init(S,1) ? lock
L) - What happens if S is initialized to 0?
- No one will be able enter the C/S, the system is
deadlock
8Binary Semaphores
- Binary semaphore is sufficient for mutex
- Binary semaphore has boolean value (not integer)
- bsem_wait() Waits until value is 1, then sets to
0 - bsem_signal() Sets value to 1, waking one
waiting process - General semaphore is also called counting
semaphore
9Ex2 Scheduling Constraints
- General case One thread waits for another to
reach some point - Example Implement thread_join()
- Parent thread creates 3 threads (if a child
thread finishes, it will call exit()) - Parent thread should not exit before all child
threads finish. Hence, parent thread calls
thread_join() three times - Lets say we have a shared sem S between parent
and child (created when child thread is created) - To what value is S initialized? 0
- How about if S is initialized to 1?
- The parent thread will finish before the last
child thread exits
Child thread exit() signal(sem)
Parent thread Thread_join() wait(sem)
10Ex3 Producer/Consumer Single Buffer
- Simplest case
- Single producer thread, single consumer thread
- Single shared buffer between producer and
consumer - Requirements
- Consumer must wait for producer to fill buffer
- Producer must wait for consumer to empty buffer
(if filled) - Requires 2 semaphores
- empty Initialize to 1
- full Initialize to 0
- What happens if
- empty is initialized to 0 produce can never
start filling the buffer - full is initialized to 1 consumer will consume a
garbage buffer (the buffer that hasnt been
filled)
Producer While (1) wait(empty) Fill(buffer)
signal(full)
Consumer While (1) wait(full) Use(buffer)
signal(empty)
11Ex4 Prod/Cons Circular Buffer
- Next case
- Single producer thread, single consumer thread
- Shared buffer with N elements between producer
and consumer - Requirements
- Consumer can run as long as there is at least one
filled element - Producer must wait for if all elements are filled
- Requires 2 semaphores
- empty Initialized to N (denotes how many empty
buffers) - full Initialized to 0 (denotes how many used
buffers) - What happens if
- empty is initialized to 1 the producer can only
fill one element (which implies the consumer can
only use one element too)
Consumer j 0 While (1) wait(full) Use(bu
fferj) j (j1)N signal(empty)
Producer i 0 While (1) wait(empty) Fill
(bufferi) i (i1)N signal(full)
12Ex4 Producer/Consumer Multiple Threads
- Final case
- Make the last case in the previous slide faster
- In the last case, we have N elements, but only
one producer thread - Make multiple producer threads, multiple consumer
threads - Shared buffer with N elements between producer
and consumer - Requirements
- A consumer thread can run if there is a filled
element - A producer thread must wait if buffer is full
- Each consumer thread must grab a unique filled
element - Each producer thread must grab a unique empty
element - Any problem?
Producer While (1) a) wait(empty) b) i
findempty(buffer) c) Fill(bufferi) d) signal(
full)
Consumer While (1) wait(full) j
findfull(buffer) Use(bufferj) signal(empty
)
13Ex4 (contd)
- The code in the previous slide is broken
- Race condition could occur
- Example 3 producer threads running the same time
- Initially empty is N (say N is 10)
- T1 (a) (b) ltpreemptedgt
- Now empty is 9, and T1 finds element 0 (i 0)
is free and needs to be filled - T2 (a) (b) ltpreemptedgt
- Now empty is 8, and T2 will fill element 0 too
- T3 (a) (b) ltpreemptedgt
- Now empty is 7, and T3 will fill element 0 too
- Problem
- Critical section for findempty and Fill is
not guarded with mutex semaphore, hence race
condition happens - When all three producer threads are done, the
free semaphore will be 3, implying there are 3
elements that have been filled (but actually
there is only one) - Observations
- empty and full semaphores are scheduling
semaphores (they do not guarantee mutual
exclusion) - Hence, need a mutex semaphore (a semaphore
initialized with 1)
14Ex4 Solutions
- Consider two possible solutions Which work?
- mutex is initialized to 1
- empty is initialized to N
- full is initialized to 0
Producer Solution 1 wait(mutex) wait(empty)
i findempty(buffer) Fill(bufferi) signal
(full) signal(mutex)
Consumer Solution 1 wait(mutex) wait(full) j
findfull(buffer) Use(bufferj) signal(em
pty) signal(mutex)
Consumer Solution 2 wait(full) wait(mutex) j
findfull(buffer) Use(bufferj) signal(mu
tex) signal(empty)
Producer Solution 2 wait(empty) wait(mutex)
i findempty(buffer) Fill(bufferi) signal
(mutex) signal(full)
15Ex4 Solution 1
Producer 1 wait(mutex) wait(empty) i
findempty(buffer) Fill(bufferi) signal(full
) signal(mutex)
Consumer 1 wait(mutex) wait(full) j
findfull(buffer) Use(bufferj) signal(empty
) signal(mutex)
- Producer
- Get the mutex lock, and then
- Check if I can fill the buffer or not. If not,
block me - Consumer
- Get the mutex lock, and then
- Check if I can empty the buffer or not. If not,
block me. - Deadlock?
- When the buffer is finally full, and a producer
wants to fill the buffer again, it will be
blocked on empty. - If blocked on empty, it means this producer is
also holding the mutex lock - A consumer wants to consume the buffer but cannot
obtain the mutex lock
16Ex4 Solution 2 (the correct one)
Consumer 2 wait(full) wait(mutex) j
findfull(buffer) Use(bufferj) signal(mutex
) signal(empty)
Producer 2 wait(empty) wait(mutex) i
findempty(buffer) Fill(bufferi) signal(mute
x) signal(full)
- Producer
- Check first if can fill the buffer or not
- If so, then get the mutex lock and fill the
buffer - Consumer
- Check first if can empty the buffer or not
- If so, get the mutex lock and use the buffer
- Observations
- Be careful in terms of what you put inside the
critical section
17Ex5 Readers/Writers Problem
- Problem
- One or more threads read and write to a shared
data - Basic concept
- No writers all readers can read data without
acquiring lock - Two writers not allowed. Hence, can only allow
one writer at a time, which means a writer must
acquire a lock before proceeding - Design
- Need to count the number of readers (if zero, the
writer can proceed)
18- Mutex 1
- Wrt 1
- Readcount 0
- Write()
- x) wait(wrt) // wait for any readers and
writers - y) doWrite()
- z) signal(wrt)
-
- Read()
- a) wait(mutex) // lock for ensuring mutex for
readcount - b) readcount
- c) if (readcount 1) // reader cannot proceed
if writer - d) wait(wrt) // use the lock
- e) signal(mutex)
- f) doRead()
19Ex5 Observations
- A writer is running
- If there is a writer, the first reader cannot
proceed. - The first reader holds the mutex lock so no
other readers can proceed - A writer enters
- If a writer enters but some readers still exist,
the last reader will signal the wrt. - One of the threads that is waiting on the wrt
thread can proceed (a reader or a writer can wait
on wrt) - E.g. a writer X is running, a writer Y and a
reader Z wants to enter. After X is done. It is
up to the thread scheduler which Y/Z to pick. If
FIFO, then the first one who is waiting will get
to run. - Runtime example
- Initially mutex1, wrt1, readcount0
- R1 a b c d e f ltpreemptedgt mutex1, wrt0,
readcount1 - R2 a b c d e f ltpreemptedgt mutex1, wrt0,
readcount2 - W1 x ltblockedgt (why? Because mutex1, wrt-1,
readcount2
20Ex5 Problem?
- Starvation?
- Is there a case of starvation? Is there a
scenario where a writer never runs? - Initially mutex1, wrt1, readcount0
- R1 a b c d e f ltpreemptedgt mutex1, wrt0,
readcount1 - R2 a b c d e f ltpreemptedgt mutex1, wrt0,
readcount2 - W1 x ltblockedgt
- R3, R4, R5 comes in and go upto (f) mutex1,
wrt0, readcount5 - R1 g h i j k mutex1, wrt0, readcount4
- R2 g h I j k mutex1, wrt0, readcount3
- Although W1 comes in before R3, R4, and R5, W1
can only proceed if there is no readers in the
system (i.e. readcount must go to 0 ?
signal(wrt)). - As long as readcount does not go to zero, the
writer can never run ? Starvation - The problem is there is no queue (semaphore
queue) which maintains the ordering of arrival - Solution
- Add another semaphore (e.g. f)
- All readers and writers must wait on f first such
that all is queued properly in semaphore f - Ex f.queue R1 R2 W1 R3 R4 R5
- New readers come after a writer (R3, R4 and R5)
cannot read unless W1 has released f - Hence the goal is when a writer is waiting on
wrt, it also holds an extra lock f
21Ex5 Fair Readers/Writers
- Mutex 1
- Wrt 1
- Readcount 0
- F 1
- Write()
- wait(f)
- wait(wrt) // While holding wrt, also hold f
so that new readers cannot - // proceed before me!
- signal(f) // If I have acquired the wrt lock,
I can release f - doWrite()
- signal(wrt)
-
- Read()
- wait(f) // all readers must go to f.queue
first if there is a waiting writer - wait(mutex)
- readcount
22Next Time
- Semaphore ? still ugly
- Use Monitors.
- But why still learn semaphore?
- Monitors are only supported in high-level
language such as Java. - Inside OS (still written in C), there is no such
nice support. Hence, must use semaphore, lock,
etc.