Basics of synchronous access
Basics of synchronous access
(Chapters 28-31)
CS 4410
Operating Systems
2
Synchronization Foundations
• Race Conditions
• Critical Sections
• Example: Too Much Milk
• Basic Hardware Primitives
• Building a SpinLock
3
Recall: Process vs. Thread
Process:
• Privilege Level
Shared
• Address Space
amongst
• Code, Data, Heap
threads
• Shared I/O resources
• One or more Threads:
• Stack
• Registers
• PC, SP
4
Two Theads, One Variable
2 threads updating a shared variable amount
• One thread wants to decrement amount by $10K
• Other thread wants to decrement amount by 50%
T1 T2
... ...
amount -= 10,000; amount *= 0.5;
... ...
9
Race Conditions are Hard to Debug
• Number of possible interleavings is huge
• Some interleavings are good
• Some interleavings are bad:
• But bad interleavings may rarely happen!
• Works 100x ≠ no race condition
• Timing dependent: small changes hide bugs
(recall: Therac-25)
10
Example: Races with Queues
• 2 concurrent enqueue() operations?
• 2 concurrent dequeue() operations?
tail head
Goals
Safety: 1 thread in a critical section at time
Liveness: all threads make it into the CS if desired
Fairness: equal chances of getting into CS
… in practice, fairness rarely guaranteed
12
Too Much Milk:
Safety, Liveness, and Fairness
with no hardware support
13
Too Much Milk Problem
2 roommates, fridge always stocked with milk
• fridge is empty → need to restock it
• don’t want to buy too much milk
Caveats
• Only communicate by a notepad on the fridge
• Notepad has cells with names, like variables:
out_to_buy_milk
0
TASK: Write the pseudo-code to ensure that at
most one roommate goes to buy milk
14
Solution #1: No Protection
T1 T2
if fridge_empty(): if fridge_empty():
buy_milk() buy_milk()
while(outtobuymilk): while(outtobuymilk):
do_nothing(); do_nothing();
if fridge_empty(): if fridge_empty():
outtobuymilk = 1 outtobuymilk = 1
buy_milk() buy_milk()
outtobuymilk = 0 outtobuymilk = 0
16
Solution #3: add two boolean flags!
one for each roommate (initially false):
blues_got_this, reds_got_this
T1 T2
blues_got_this = 1 reds_got_this = 1
if !reds_got_this and if !blues_got_this and
fridge_empty(): fridge_empty():
buy_milk() buy_milk()
blues_got_this = 0 reds_got_this = 0
Safety: Only one person (at most) buys milk
Liveness: If milk is needed, someone eventually buys it.
Fairness: Roommates equally likely to go to buy milk.
Safe? Live? Fair?
17
Solution #4: asymmetric flags!
one for each roommate (initially false):
blues_got_this, reds_got_this
T1 T2
blues_got_this = 1 reds_got_this = 1
while reds_got_this: if not blues_got_this:
do_nothing() if fridge_empty():
if fridge_empty(): buy_milk()
buy_milk() reds_got_this = 0
blues_got_this = 0
while(TAS(&buyingmilk)) while(TAS(&buyingmilk))
do_nothing(); do_nothing();
if fridge_empty(): if fridge_empty():
buy_milk() buy_milk()
buyingmilk := 0 buyingmilk := 0
release(int *lock) {
*lock = 0;
}
22
Buying Milk with Locks
Shared lock: int buyingmilk, initially 0
T1 T2
acquire(&buyingmilk); acquire(&buyingmilk);
if fridge_empty(): if fridge_empty():
buy_milk() buy_milk()
release(&buyingmilk); release(&buyingmilk);
25
• Foundations
• Semaphores
• Monitors & Condition
Variables
26
Semaphores
• Definition
• Binary Semaphores
• Counting Semaphores
• Classic Sync. Problems (w/Semaphores)
- Producer-Consumer (w/ a bounded buffer)
- Readers/Writers Problem
• Classic Mistakes with Semaphores
27
[Dijkstra 1962]
What is a Semaphore?
Dijkstra introduced in the THE Operating System
Stateful:
• a value (incremented/decremented atomically)
• a queue
• a lock
Interface:
• Init(starting value)
• P (procure): decrement, “consume” or “start using”
• V (vacate): increment, “produce” or “stop using”
No operation to read the value!
28
Dutch 4410: P = Probeer (‘Try'), V = Verhoog ('Increment', 'Increase by one')
Semantics of P and V
P(): P() {
• wait until value >0 while(n <= 0)
;
• when so, decrement n -= 1;
VALUE by 1 }
V(): V() {
• increment VALUE by 1 n += 1;
}
S.P() S.P()
CriticalSection() CriticalSection()
S.V() S.V()
31
Example: A simple mutex
Semaphore S
S.init(1)
P() {
while(n <= 0)
S.P() V() { ;
CriticalSection() n += 1; n -= 1;
S.V() } }
32
Counting Semaphores
Sema count can be any integer
• Used for signaling or counting resources
• Typically:
• one thread performs P() to await an event
• another thread performs V() to alert waiting
thread that event has occurred
Semaphore packetarrived
packetarrived.init(0)
T1 T2
ReceivingThread: PrintingThread:
pkt = get_packet() packetarrived.P();
enqueue(packetq, pkt); pkt = dequeue(packetq);
packetarrived.V(); print(pkt);
33
Semaphore’s count:
• must be initialized!
• keeps state
• reflects the sequence of past operations
• >0 reflects number of future P operations
that will succeed
in out
Bounded buffer: size —N entries—
Producer process writes data to buffer
• Writes to in and moves rightwards
36
Shared:
Starter Code: No Protection int buf[N];
int in, out;
// remove item
// add item to buffer
int consume() {
void produce(int item) {
int item = buf[out];
buf[in] = item;
out = (out+1)%N;
in = (in+1)%N;
return item;
}
}
Problems:
1. Unprotected shared state (multiple producers/consumers)
2. Inventory:
• Consumer could consume when nothing is there!
• Producer could overwrite not-yet-consumed data!
37
Part 1: Guard Shared Resources
Shared:
int buf[N];
int in = 0, out = 0;
Semaphore mutex_in(1), mutex_out(1);
Cons:
• Still seems complicated: is it correct?
• Not so readable
• Easy to introduce bugs
41
Invariant
Shared:
0 ≤ in – out ≤ N
int buf[N];
int in = 0, out = 0;
Semaphore mutex_in(1), mutex_out(1);
Semaphore empty(N), filled(0);
43
Readers-Writers Specifications
N threads share 1 object in memory
• Some write: 1 writer active at a time
• Some read: n readers active simultaneously
Insight: generalizes the critical section concept
Implementation Questions:
1. Writer is active. Combo of readers/writers arrive.
Who should get in next?
2. Writer is waiting. Endless of # of readers come.
Fair for them to become active?
46
Readers-Writers: Assessing the Solution
When readers active no writer can enter ✔︎
• Writers wait @ rw_lock.P()
When writer is active nobody can enter ✔︎
• Any other reader or writer will wait (where?)
Back-and-forth isn’t so fair:
• Any number of readers can enter in a row
• Readers can “starve” writers
47
Semaphores
• Definition
• Binary Semaphores
• Counting Semaphores
• Classic Sync. Problems (w/Semaphores)
- Producer-Consumer (w/ a bounded buffer)
- Readers/Writers Problem
• Classic Mistakes with Semaphores
48
Classic Semaphore Mistakes
P(S) I I stuck on 2nd P(). Subsequent
CS processes freeze up on 1st P().
P(S) ︎typo
Undermines mutex:
︎typo J • J doesn’t get permission via P()
V(S)
• “extra” V()s allow other processes
CS into the CS inappropriately
V(S)
Next call to P() will freeze up.
P(S) K Confusing because the other process
CS ︎omission could be correct but hangs when you
use a debugger to look at its state!
P(S) L
Conditional code can change code
if(x) return; flow in the CS. Caused by code
CS updates (bug fixes, etc.) by someone
V(S) other than original author of code.49
Semaphores Considered Harmful
“During system conception … we used the
semaphores in two completely different ways.
The difference is so marked that, looking back,
one wonders whether it was really fair to
present the two ways as uses of the very same
primitives. On the one hand, we have the
semaphores used for mutual exclusion, on the
other hand, the private semaphores.”
51
• Foundations
• Semaphores
• Monitors &
Condition Variables
52
Producer-Consumer char buf[SIZE];
int n=0, tail=0, head=0;
with locks lock l;
produce(char ch) {
l.acquire()
while(n == SIZE):
l.release(); l.acquire()
buf[head] = ch;
head = (head+1)%SIZE;
n++;
l.release();
}
char consume() {
l.acquire()
while(n == 0):
l.release(); l.acquire()
ch = buf[tail];
tail = (tail+1)%SIZE;
n--;
l.release;
return ch;
} 53
Thou
shalt not
busy-wait!
54
CONCURRENT APPLICATIONS
...
SYNCHRONIZATION OBJECTS
Locks Semaphores Condition Variables Monitors
ATOMIC INSTRUCTIONS
Interrupt Disable Atomic R/W Instructions
HARDWARE
Multiple Processors Hardware Interrupts
55
Monitors & Condition Variables
• Definition
• Simple Monitor Example
• Implementation
• Classic Sync. Problems with Monitors
- Bounded Buffer Producer-Consumer
- Readers/Writers Problems
- Barrier Synchronization
• Semantics & Semaphore Comparisons
• Classic Mistakes with Monitors
56
Monitor Semantics guarantee mutual exclusion
Only one thread can execute monitor procedure at any time
(aka “in the monitor”)
can only access shared
in the abstract: data via a monitor
Monitor monitor_name
{ procedure
// shared variable declarations for example:
Monitor bounded_buffer
procedure P1() { {
} int in=0, out=0, nElem=0;
int buffer[N];
e ra tion
procedure P2() {
n e o p m e
consume() {
l y o t a t i
on
}
}
c u t e a
.
e x e
. produce() { c an
procedure PN() { }
}
}
initialization_code() {
}
} 57
Producer-Consumer Revisited
Problems:
1. Unprotected shared state (multiple producers/consumers)
Solved via Monitor.
Only 1 thread allowed in at a time.
• Only one thread can execute monitor procedure at any time
• If second thread invokes monitor procedure at that time, it will
block and wait for entry to the monitor.
• If thread within a monitor blocks, another can enter
2. Inventory:
• Consumer could consume when nothing is there!
• Producer could overwrite not-yet-consumed data!
60
Condition Variables Live in the Monitor
Abstract Data Type for handling
shared resources, comprising:
62
Kid and Cook Threads
Running
Ready
Monitor BurgerKing {
Lock mlock
int numburgers = 0
condition hungrykid
64
Language Support
Can be embedded in programming language:
• Compiler adds synchronization code, enforced
at runtime
• Mesa/Cedar from Xerox PARC
• Java: synchronized, wait, notify, notifyall
• C#: lock, wait (with timeouts) , pulse, pulseall
• Python: acquire, release, wait, notify, notifyAll
class BurgerKingMonitor(MP):
def __init__(self):
MP.__init__(self,None)
self.lock = Lock(“monitor lock”)
self.hungrykid = self.lock.Condition(“hungry kid”)
self.nBurgers = self.Shared(“num burgers”, 0)
67
Monitors in “4410 Python” : kid_eat
def kid_eat(self): Python
with self.lock:
while self.nBurgers == 0:
self.hungrykid.wait()
self.nBurgers = self.nBurgers - 1
69
Producer-Consumer Monitor Producer_Consumer {
char buf[SIZE];
int n=0, tail=0, head=0;
What if no thread is waiting condition not_empty, not_full;
when notify() called? produce(char ch) {
while(n == SIZE):
wait(not_full);
Then signal is a nop. buf[head] = ch;
Very different from calling head = (head+1)%SIZE;
n++;
V() on a semaphore – notify(not_empty);
semaphores remember }
char consume() {
how many times V() was while(n == 0):
called! wait(not_empty);
ch = buf[tail];
tail = (tail+1)%SIZE;
n--;
notify(not_full);
return ch;
}
} 70
Readers and Writers
Monitor ReadersNWriters {
EndWrite()
void EndRead()
with monitor.lock:
with monitor.lock:
nWriters = 0
--nReaders;
if WaitingWriters > 0
if (nReaders==0 and waitingWriters>0)
canWrite.signal();
canWrite.signal();
else if waitingReaders > 0
canRead.broadcast();
}
71
Understanding the Solution
A writer can enter if: A reader can enter if:
• no other active writer • no active writer
&& &&
• no active readers • no waiting writers
73
Barrier Synchronization
• Important synchronization primitive in high-
performance parallel programs
• nThreads threads divvy up work, run rounds of
computations separated by barriers.
• could fork & wait but
– thread startup costs
– waste of a warm cache
def checkin():
with self.lock:
nArrived++
if nArrived < nThreads:
while nArrived < nThreads and nArrived >
0:
allCheckedIn.wait()
else:
allCheckedIn.broadcast()
nArrived = 0
What’s wrong with this?
75
Monitors & Condition Variables
• Definition
• Simple Monitor Example
• Implementation
• Classic Sync. Problems with Monitors
- Bounded Buffer Producer-Consumer
- Readers/Writers Problems
- Barrier Synchronization
• Semantics & Semaphore Comparisons
• Classic Mistakes with Monitors
76
CV semantics: Hansen vs. Hoare
The condition variables we have defined
obey Brinch Hansen (or Mesa) semantics
• signaled thread is moved to ready list, but
not guaranteed to run right away
80
wikipedia.org
What are the implications?
Hansen/Mesa Hoare
signal() and broadcast() are hints Signaling is atomic with the
• adding them affects resumption of waiting thread
performance, never safety • shared state cannot change
Shared state must be checked in before waiting thread
resumed
a loop (could have changed)
• robust to spurious wakeups Shared state can be checked
using an if statement
Simple implementation
• no special code for thread Easier to prove liveness
scheduling or acquiring lock Tricky to implement
Used in most systems Used in most books
Sponsored by a Turing Award Sponsored by a Turing Award
(Butler Lampson) (Tony Hoare)
81
Condition Variables vs. Semaphores
Access to monitor is controlled by a lock. To call wait or
signal, thread must be in monitor (= have lock).
Wait vs. P:
• Semaphore P() blocks thread only if value < 1
• wait always blocks & gives up the monitor lock
Signal vs. V: causes waiting thread to wake up
• V() increments ➙ future threads don't wait on P()
• No waiting thread ➙ signal = nop
• Condition variables have no history!
Monitors easier than semaphores
• Lock acquire/release are implicit, cannot be forgotten
• Condition for which threads are waiting explicitly in code
82
Pros of Condition Variables
Condition variables force the actual conditions that a
thread is waiting for to be made explicit in the code
• comparison preceding the “wait()” call concisely
specifies what the thread is waiting for
83
12 Commandments of Synchronization
87
#11: Thou shalt not split predicates
with lock: What is wrong with this?
while not condA:
condA_cv.wait()
while not condB:
condB_cv.wait()
Better:
with lock:
while not condA or not condB:
if not condA:
condA_cv.wait()
if not condB:
condB_cv.wait()
88
A few more guidelines
• Use consistent structure
• Always hold lock when using a
condition variable
• Never spin in sleep()
89
d e a l
Conclusion: Race Conditions are a big pain!
Several ways to handle them
• each has its own pros and cons
def checkin():
nArrived++
if nArrived < nThreads: // not everyone has checked in
while nArrived < nThreads:
allCheckedIn.wait() // wait for everyone to check in
else:
nLeaving = 0 // this thread is the last to arrive
allCheckedIn.broadcast() // tell everyone we’re all here!
nLeaving++
if nLeaving < nThreads: // not everyone has left yet
while nLeaving < nThreads:
allLeaving.wait() // wait for everyone to leave
else:
nArrived = 0 // this thread is the last to leave
allLeaving.broadcast() // tell everyone we’re outta here!