Outline
- Recap
- Better primitives.
- Higher-level managers.
- Stones, bowls and semaphores.
- Operating systems assist.
- Still higher-level managers.
- ADTs, classes, and monitors.
Where are We?
- Tame uncoordinated, concurrent, shared access.
- Solve through concurrency, move to coordination.
- Concurrency tamed by mutual exclusion in critical sections.
- Via user-land code or interrupt disabling.
What’s Wrong?
- Peterson’s algorithm.
- A spin lock.
- Disconnected from the OS.
- Unwise scheduling decisions on both sides.
- Disabling interrupts.
- Touchy and inflexible.
- Monstrously difficult errors to find and fix.
- Better mutual exclusion primitives.
What’s Next?
Stones and Bowls
- Some rules for critical section cs:
- There’s a bowl at enter cs.
- A thread in cs must hold the stone.
- The thread at exit cs puts the stone in the bowl.
- A thread at enter cs wanting into cs must take the stone from
the bowl first.
- These rules provide mutual exclusion, progress, and starvation. And
independence.
Modern Stones and Bowls
- The modern version of the stone in a bowl is the
test-and-set
instruction.
- The bowl is a boolean variable b.
- If b = true, the bowl holds the stone.
- If b = false, the bowl is empty.
-
test-and-set
is a non-interruptable, non-privileged instruction in
modern architectures.
- SPARC v9
ldstub
instruction.
test-and-set
- The effect of
test-and-set
is
bool test-and-set(bool * bowl)
bool b = *bowl |
*bowl = false |
return b |
- Set a variable to false and return the variable’s previous value.
- If the stone’s in the bowl,
test-and-set
returns true and the bowl
is empty.
- Otherwise
test-and-set
returns false.
Test-and-Set Critical Sections
- A
test-and-set
critical section:
bool bowl-csi = true
enter csi
while !test-and-set(bowl-csi) { }
exit csi
bowl-csi = true
-
bowl
is usually called lock
.
Binary Semaphores
Binary Semaphore Example
widget q[N]
q-size = 0
head = tail = 0
BinarySemaphore bowl(true)
p()
loop
w = f()
while q-size ≡ N {}
q[tail++] = w
bowl.take()
++q-size
bowl.put()
| c()
loop
while q-size ≡ 0 {}
w = q[head++]
bowl.take()
--q-size
bowl.put()
g(w)
|
Tucking in the Loose Bits
- The binary semaphore solution has a lot of machinery hanging out.
p()
loop
w = f()
while q-size ≡ N {}
q[tail++] = w
bowl.take()
++q-size
bowl.put()
|
- How can the details be neatened up?
Hummmm...
Counting Semaphores
class CountingSemaphore
private unsigned bowl
private BinarySemaphore mutex(true)
CountingSemaphore(unsigned c)
bowl = c
|
void put()
mutex.take()
++bowl
mutex.put()
unsigned count()
return bowl
| void take()
bool done = false
while !done
mutex.take()
if bowl > 0
--bowl
done = true
mutex.put()
|
Counting Semaphore Example
widget q[N]
head = tail = 0
CountingSemaphore bowl(0)
p()
loop
while bowl.count() ≡ N {}
q[tail] = f()
bowl.put();
tail = (tail + 1) mod N
| c()
loop
bowl.take()
w = q[head]
head = (head + 1) mod N
g(w)
|
Waiting, Not Spinning
- Spinning wastes resources, usually.
- Move semaphores into the operating system.
- Threads can now block (leave the CPU) instead of spin.
- Better CPU utilization, but higher overhead (system calls).
- Usually known as blocking semaphores.
Blocking Semaphores
class BinarySemaphore
BinarySemaphore(boolean b)
bowl = b
void take()
while true
if bowl
bowl = false ; break
else
pre-empt caller to waiters
void put()
bowl = true
if !waiters.empty()
schedule a waiter
private boolean bowl
private queue waiters
Are We Done?
- I dunno. Does this look ok?
BinarySemaphore bowl(true)
\(\vdots\) |
bowl.put()
cs
bowl.take()
| \(\vdots\) |
|
\(\vdots\) |
bowl.put()
cs
bowl.put()
| \(\vdots\) |
|
- Semaphore programming is low-level.
- Hard to get right, hard to keep right, hard to debug.
Improvements
- Hide more machinery.
- No flags, or stones, or bowls.
- Fool-proof what remains.
- No putting before taking, no taking with no putting.
- Provide more meaningful interpretations for concurrent control.
- Concurrency solutions should look like the problem, not patches of
mutual exclusion.
For Example
- The last producer-consumer solution.
queue<widget> q
p()
loop
q.add(f())
| c()
loop
g(q.remove())
|
- Can we get there? How do we get there?
Hiding Shared State
- The first step is to hide shared state.
- Abstract details.
- Protected and controlled access.
- Abstract data types (ADTs) do this job nicely.
- Second step: turn the ADT into a critical section.
- That is, provide mutual exclusion in the ADT.
- At most one thread in an ADT instance at any time.
Mutually Exclusive ADTs
ADT
Shared private state
private BinarySemaphore mutex(true)
\(\vdots\) |
operi(...)
mutex.put()
cs
mutex.take()
|
\(\vdots\) |
- At most one operation is executing at any time.
Queue Example
ADT queue<T>
T q[N]
head = tail = 0
q-size = 0
void add(T e)
q[tail] = e
tail = tail+1 mod N
++q-size
| T remove()
T e = q[head]
head = head+1 mod N
--q-size
return T
|
Strategic Waiting
- Adders need a non-full queue; removers need a non-empty queue.
- Can’t semaphores take care of this?
ADT queue<T>
T q[N] ; head = tail = 0 ; q-size = 0
BinarySemaphore non-empty(false), non-full(true)
void add(T e)
non-full.take()
q[tail] = e
tail = tail+1 mod N
++q-size
non-empty.put()
| T remove()
non-empty.take()
T e = q[head]
head = head+1 mod N
--q-size
non-full.put()
return T
|
Deadlock!
- Waiting in a critical section ADT is a bad idea.
- The thread waiting inside can’t do anything.
- The threads outside can’t come in.
- This is known as deadlock.
- The idea is fine, but semaphores aren’t the right tool.
- Invoke the wait inside the ADT.
- Wait outside the ADT.
Condition Variables
- Condition variables are modified binary semaphores.
- Waiting threads are moved outside the ADT.
- Also
put()
= signal()
, take()
= wait()
.

Monitors
- ADTs, mutual exclusion, and condition variables combine to form
monitors.
- Synchronized classes in Java.
- Monitors have a wide range of semantics.
- For almost every part of the monitor.
- Unsurprisingly, monitors are not the last word.
Queue Monitor
monitor queue<T>
T q[N]
head = tail = 0
q-size = 0
condition qUnfull(true), qUnempty(false)
void put(T e)
if q-size ≡ N
qUnfull.wait()
q[tail] = e
tail = tail+1 mod N
++q-size
qUnempty.signal()
| T get()
qUnempty.wait()
T e = q[head]
head = head+1 mod N
--q-size
qUnfull.signal()
return T
|
Summary
- Hardware provides fast, simple mutual exclusion.
- Operating systems provide intelligent waiting.
- Semaphores are the bottom of the management hierarchy.
- Monitors provide higher-level concurrency management.
- Even higher-level managers are needed.
References
- Process Scheduling and Unix Semaphores by Neil Dunstan and Ivan Fris in
Software Practice and Experience, October 1995.
- Java’s Insecure
Parallelism by Per Brinch Hansen in ACM Sigplan Notices, April 1999.
- Alphonse, Wait Here For
My Signal! by Stephen Hartley in Proceedings of the Thirtieth SIGCSE
Technical Symposium on Computer Science Education, March 1999.
Credits
This page last modified on 2014 October 9.
|
|