0% found this document useful (0 votes)
4 views

Basics of synchronous access

The document covers synchronization in operating systems, focusing on race conditions, critical sections, and various synchronization mechanisms such as semaphores and locks. It discusses the challenges of ensuring safety, liveness, and fairness in concurrent programming, illustrated through examples like the 'Too Much Milk' problem and the producer-consumer problem. The document also highlights the implementation of semaphores and their role in managing shared resources effectively.

Uploaded by

Harsh Kr
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

Basics of synchronous access

The document covers synchronization in operating systems, focusing on race conditions, critical sections, and various synchronization mechanisms such as semaphores and locks. It discusses the challenges of ensuring safety, liveness, and fairness in concurrent programming, illustrated through examples like the 'Too Much Milk' problem and the producer-consumer problem. The document also highlights the implementation of semaphores and their role in managing shared resources effectively.

Uploaded by

Harsh Kr
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 92

Synchronization

(Chapters 28-31)

CS 4410
Operating Systems

[R. Agarwal, L. Alvisi, A. Bracy, M. George, E. Sirer, R. Van Renesse]


• Foundations
• Semaphores
• Monitors & Condition
Variables

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;
... ...

Memory amount 100,000


What happens when both threads are running?
5
Two Theads, One Variable
Might execute like this:
T2
. . .
r2 = load from amount
r2 = 0.5 * r2
T1 store r2 to amount
. . .
r1 = load from amount . . .
r1 = r1 – 10,000
store r1 to amount
. . .

Memory amount 40,000

Or vice versa (T1 then T2 à 45,000)…


either way is fine… 6
Two Theads, One Variable
Or it might execute like this:
T2
. . .
T1 r2 = load from amount
. . .
r1 = load from amount
r1 = r1 – 10,000 . . .
store r1 to amount
. . . r2 = 0.5 * r2
store r2 to amount
. . .

Memory amount 50,000


Lost Update!
Wrong ..and very difficult to debug 7
Race Conditions
= timing dependent error involving shared state
• Once thread A starts, it needs to “race” to finish
• Whether race condition happens depends on
thread schedule
• Different “schedules” or “interleavings” exist
(total order on machine instructions)

All possible interleavings should


be safe!
8
Problems with Sequential Reasoning
1. Program execution depends on the possible
interleavings of threads’ access to shared
state.

2. Program execution can be nondeterministic.

3. Compilers and processor hardware can


reorder instructions.

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

What could possibly go wrong?


11
Critical Section
Must be atomic due to shared memory access
T1 T2
. . . . . .
CSEnter(); CSEnter();
Critical section Critical section
CSExit(); CSExit();
. . . . . .

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()

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? 15


Solution #2: add a boolean flag
outtobuymilk initially false
T1 T2

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

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?

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

Safe? Live? Fair?


‒ complicated (and this is a simple example!)
‒ hard to ascertain that it is correct
‒ asymmetric code is hard to generalize & unfair 18
Last Solution: Peterson’s Solution
another flag turn {blue, red}
T1 T2
blues_got_this = 1 reds_got_this = 1
turn = red turn = blue
while (reds_got_this while (blues_got_this
and turn==red): and turn==blue):
do_nothing() do_nothing()
if fridge_empty(): if fridge_empty():
buy_milk() buy_milk()
blues_got_this = 0 reds_got_this = 0

Safe? Live? Fair?


‒ complicated (and this is a simple example!)
‒ hard to ascertain that it is correct
‒ hard to generalize 19
Hardware Solution
• HW primitives to provide mutual exclusion
• A machine instruction (part of the ISA!) that:
• Reads & updates a memory location
• Is atomic (other cores can’t see intermediate state)
• Example: Test-And-Set
1 instruction with the following semantics:
ATOMIC int TestAndSet(int *var) {
int oldVal = *var;
*var = 1;
return oldVal;
}

sets the value to 1, returns former value


20
Buying Milk with TAS
Shared variable: int buyingmilk, initially
0
T1 T2

while(TAS(&buyingmilk)) while(TAS(&buyingmilk))
do_nothing(); do_nothing();
if fridge_empty(): if fridge_empty():
buy_milk() buy_milk()
buyingmilk := 0 buyingmilk := 0

A little hard on the eyes. Can we do better?


21
Enter: Locks!
acquire(int *lock) {
while(test_and_set(lock))
/* do nothing */;
}

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);

Now we’re getting somewhere!


Is anyone not happy with this?
23
Thou
shalt not
busy-wait!
24
Not just any locks: SpinLocks
Participants not in critical section must spin
→ wasting CPU cycles
• Replace the “do nothing” loop with a “yield()”?
• Threads would still be scheduled and descheduled
(context switches are expensive)

Need a better primitive:


• allows one thread to pass through
• all others sleep until they can execute again

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;
}

These are the semantics,


but how can we make this efficient?
(doesn’t this look like a spinlock?!?) 29
Implementation of P and V
P(): P() {
• block (sit on Q) til n > 0 while(n <= 0)
• when so, decrement VALUE ;
by 1 n -= 1;
}
V():
• increment VALUE by 1 V() {
• resume a thread waiting on n += 1;
Q (if any) }

Okay this looks efficient, but how is this safe?


(that’s what the lock is for – both P&V need to TAS the lock)
30
Binary Semaphore
Semaphore value is either 0 or 1
• Used for mutual exclusion
(semaphore as a more efficient lock)
• Initially 1 in that case
Semaphore S
S.init(1)
T1 T2

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

Not possible to:


• read the count
• grab multiple semaphores at same
time
• decrement/increment by more than 1! 34
Producer-Consumer Problem
2+ threads communicate:
some threads produce data that others consume
0 N-1

in out
Bounded buffer: size —N entries—
Producer process writes data to buffer
• Writes to in and moves rightwards

Consumer process reads data from buffer


• Reads from out and moves rightwards
35
Producer-Consumer Applications
• Pre-processor produces source file for
compiler’s parser
• Data from bar-code reader consumed by
device driver
• File data: computer à printer spooler à line
printer device driver
• Web server produces data consumed by
client’s web browser
• “pipe” ( | ) in Unix >cat file | sort | more

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);

// add item to buffer // remove item


void produce(int item) int consume()
{ {
mutex_in.P(); mutex_out.P();
buf[in] = item; o m i c int item = buf[out];
now a t
in = (in+1)%N; out = (out+1)%N;
mutex_in.V(); mutex_out.V();
return item;
} }
38
Part 2: Manage the Inventory
Shared:
int buf[N];
int in = 0, out = 0;
Semaphore mutex_in(1), mutex_out(1);
Semaphore empty(N), filled(0);

void produce(int item) int consume()


{ {
empty.P(); //need space filled.P(); //need item
mutex_in.P(); mutex_out.P();
buf[in] = item; int item = buf[out];
in = (in+1)%N; out = (out+1)%N;
mutex_in.V(); mutex_out.V();
filled.V(); //new item! empty.V(); //more space!
return item;
}
} 39
Sanity checks 1. Is there a V for every P?
2. Mutex initialized to 1?
Shared: 3. Mutex P&V in same thread?
int buf[N];
int in = 0, out = 0;
Semaphore mutex_in(1), mutex_out(1);
Semaphore empty(N), filled(0);

void produce(int item) int consume()


{ {
empty.P(); //need space filled.P(); //need item
mutex_in.P(); mutex_out.P();
buf[in] = item; int item = buf[out];
in = (in+1)%N; out = (out+1)%N;
mutex_in.V(); mutex_out.V();
filled.V(); //new item! empty.V(); //more space!
return item;
}
} 40
Producer-consumer: How did we do?
Pros:
• Live & Safe (& Fair)
• No Busy Waiting! (is this true?)
• Scales nicely

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);

void produce(int item) int consume()


{ {
empty.P(); //need space filled.P(); //need item
mutex_in.P(); mutex_out.P();
buf[in%N] = item; int item = buf[out%N];
in += 1; out += 1;
mutex_in.V(); mutex_out.V();
filled.V(); //new item! empty.V(); //more space!
return item;
}
} 42
[Courtois+ 1971]
Readers-Writers Problem
Models access to a database: shared data that
some threads read and other threads write
At any time, want to allow:
• multiple concurrent readers —OR—(exclusive)
• only a single writer

Example: making an airline reservation


• Browse flights: web site acts as a reader
• Reserve a seat: web site has to write into
database (to make the reservation)

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?

For now: back-and-forth turn-taking:


• If a reader is waiting, readers get in next
• If a writer is waiting, one writer gets in next
44
Readers-Writers Solution
int read()
Shared: {
int rcount = 0; count_mutex.P();
Semaphore count_mutex(1); rcount++;
Semaphore rw_lock(1); if (rcount == 1)
rw_lock.P();
count_mutex.V();
. . .
void write() /* perform read */
rw_lock.P();
. . .
. . . count_mutex.P();
/* perform write */ rcount--;
. . .
if (rcount == 0)
rw_lock.V(); rw_lock.V();
} count_mutex.V();
} 45
Readers-Writers: Understanding the Solution
If there is a writer:
• First reader blocks on rw_lock
• Other readers block on mutex

Once a reader is active, all readers get to go through


• Which reader gets in first?

The last reader to exit signals a writer


• If no writer, then readers can continue

If readers and writers waiting on rw_lock & writer exits


• Who gets to go in first?

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

Fair back-and-forth semaphore solution is tricky!


• Try it! (don’t spend too much time…)

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.”

— Dijkstra “The structure of the ’THE’-


Multiprogramming System” Communications of the
ACM v. 11 n. 5 May 1968. 50
Semaphores NOT to the rescue!
These are “low-level” primitives. Small errors:
• Easily bring system to grinding halt
• Very difficult to debug
Two usage models:
• Mutual exclusion: “real” abstraction is a critical
section
• Communication: threads use semaphores to
communicate (e.g., bounded buffer example)
Simplification: Provide concurrency support in
compiler
à Enter Monitors

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!

What about these?


à Enter Condition Variables 58
Condition Variables
A mechanism to wait for events

3 operations on Condition Variable x


• x.wait(): sleep until woken up (could wake
up on your own)
• x.signal(): wake at least one process
waiting on condition (if there is one). No
history associated with signal.
• x.broadcast(): wake all processes waiting on
condition

!! NOT the same thing as UNIX wait & signal !! 59


Using Condition Variables
You must hold the monitor lock to call these
operations.
To wait for some condition:
while not some_predicate():
CV.wait()
• atomically releases monitor lock & yields processor
• as CV.wait() returns, lock automatically reacquired

When the condition becomes satisfied:


CV.broadcast(): wakes up all threads
CV.signal(): wakes up at least one thread

60
Condition Variables Live in the Monitor
Abstract Data Type for handling
shared resources, comprising:

1. Shared Private Data


• the resource
• can only be accessed from in the monitor
2. Procedures operating on data
• gateway to the resource
• can only act on data local to the monitor
3. Synchronization primitives
• among threads that access the procedures
[Hoare 1974] 61
Types of Wait Queues

Monitors have two kinds of “wait” queues


• Entry to the monitor: a queue of
threads waiting to obtain mutual exclusion &
enter
• Condition variables: each condition
variable has a queue of threads waiting on
the associated condition

62
Kid and Cook Threads
Running
Ready

Monitor BurgerKing {
Lock mlock

int numburgers = 0
condition hungrykid

kid_main() { kid_eat: cook_main() {


with mlock:
play_w_legos() while (numburgers==0) wake()
BK.kid_eat() hungrykid.wait() shower()
bathe() numburgers -= 1 drive_to_work()
make_robots() while(not_5pm)
BK.kid_eat() makeburger: BK.makeburger()
facetime_Karthik() with mlock: drive_to_home()
facetime_oma() ++numburger watch_got()
BK.kid_eat() hungrykid.signal() sleep()
} 63
} }
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

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

Monitors easier & safer than semaphores


• Compiler can check
• Lock acquire and release are implicit and
cannot be forgotten
65
Monitors in Python
class BK:
def __init__(self):
self.lock = Lock()
self.hungrykid = Condition(self.lock)
self.nBurgers= 0
wait h e n c a l l e d
e a s e s l o c kw tu rn
• r e l wh e n i t r e
def kid_eat(self): i r e s l o c k
with self.lock: • re-acqu
while self.nBurgers == 0:
self.hungrykid.wait()
self.nBurgers = self.nBurgers - 1 fy()
noti All()
( ) ➙ tify
def make_burger(self): al no
sign ast) ➙
with self.lock: roa dc
b
self.nBurgers = self.nBurgers + 1
self.hungrykid.notify() 66
Monitors in “4410 Python” : __init__
class BK: Python
def __init__(self):
self.lock = Lock()
self.hungrykid = Condition(self.lock)
self.nBurgers= 0

from rvr import MP, MPthread 4410 Python

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

def kid_eat(self): 4410 Python


with self.lock:
while (self.nBurgers.read() == 0):
self.hugryKid.wait()
self.nBurgers.dec()

We do this for helpful feedback: Look in the A2/doc


• from auto-grader directory for details
• from debugger and example code.
68
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

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 {

int waitingWriters=0, waitingReaders=0, nReaders=0, nWriters=0;


Condition canRead, canWrite;
void BeginRead()
BeginWrite()
with monitor.lock:
with monitor.lock:
++waitingReaders
++waitingWriters
while (nWriters>0 or waitingWriters>0)
while (nWriters >0 or nReaders >0)
canRead.wait();
canWrite.wait();
--waitingReaders
--waitingWriters
++nReaders
nWriters = 1;

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

When a writer finishes: Last reader finishes:


check for waiting writers • it lets 1 writer in
Y ➙ lets one enter (if any)
N ➙ let all readers enter
72
Fair?
• If a writer is active or waiting, readers
queue up
• If a reader (or another writer) is active,
writers queue up

… gives preference to writers, which is


often what you want

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

Create n threads & a barrier.


Each thread does round1()
barrier.checkin()
Each thread does round2()
barrier.checkin()
74
Checkin with 1 condition variable
self.allCheckedIn = Condition(self.lock)

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

Hoare proposes an alternative semantics


• signaling thread is suspended and,
atomically, ownership of the lock is passed
to one of the waiting threads, whose
execution is immediately resumed
77
Kid and Cook Threads Revisited
Hoare vs. Mesa semantics
• What happens if there are lots of
kids?
Monitor BurgerKing {
Lock mlock

Ready int numburgers = 0


condition hungrykid

kid_main() { kid_eat: cook_main() {


with mlock:
play_w_legos() while (numburgers==0) wake()
BK.kid_eat() hungrykid.wait() shower()
bathe() numburgers -= 1 drive_to_work()
make_robots() while(not_5pm)
BK.kid_eat() makeburger: BK.makeburger()
facetime_Karthik() with mlock: drive_to_home()
facetime_oma() ++numburger watch_got()
BK.kid_eat() hungrykid.signal() sleep()
} 78
} }
Hoare vs. Mesa/Hansen Semantics
Hoare Semantics: monitor lock
transferred directly from signaling thread to
woken up thread
+ clean semantics, easy to reason about
– not desirable to force signaling thread to give
monitor lock immediately to woken up thread
– confounds scheduling with synchronization,
penalizes threads

Mesa/Hansen Semantics: puts a woken


up thread on the monitor entry queue, but
does not immediately run that thread, or
transfer the monitor lock
79
Which is Mesa/Hansen? Which is Hoare?

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

Condition variables themselves have no state à


monitor must explicitly keep the state that is
important for synchronization
• This is a good thing!

83
12 Commandments of Synchronization

1. Thou shalt name your synchronization variables properly.


2. Thou shalt not violate abstraction boundaries nor try to
change the semantics of synchronization primitives.
3. Thou shalt use monitors and condition variables instead of
semaphores whenever possible.
4. Thou shalt not mix semaphores and condition variables.
5. Thou shalt not busy-wait.
6. All shared state must be protected.
7. Thou shalt grab the monitor lock upon entry to, and release
it upon exit from, a procedure.
84
12 Commandments of Synchronization

8. Honor thy shared data with an invariant, which your code


may assume holds when a lock is successfully acquired and
your code must make true before the lock is released.
9. Thou shalt cover thy naked waits.
10. Thou shalt guard your wait predicates in a while loop. Thou
shalt never guard a wait statement with an if statement.
11. Thou shalt not split predicates.
12. Thou shalt help make the world a better place for the
creator’s mighty synchronization vision.
85
#9: Cover Thy Naked Waits
while not some_predicate():
CV.wait()

What’s wrong with this?


random_fn1()
CV.wait() How about this?
random_fn2() with self.lock:
a=False
while not a:
self.cv.wait()
a=True 86
#10: Guard your wait in a while loop

What is wrong with this?


if not some_predicate():
CV.wait()

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

Programming language support simplifies writing


multithreaded applications
• Python condition variables
• Java and C# support at most one condition variable
per object, so are slightly more limited

Some program analysis tools automate checking


• make sure code is using synchronization correctly
• hard part is defining “correct”
90
Lecture Schedule
1. Foundations, slides 1-26
• Activity: too much milk
2. Semaphores, slides 27-42, 48-51
• Activity: Producer-Consumer w/Semaphores
3. Monitors & Condition Variables, 52-69
• Activity: before monitors, do Rdrs/Writer (43-47
that you left out before), Producer Consumer
M&CVs
4. CV Semantics, vs. Semaphores, 76-83
• Activity: Readers/Writer with M&CVs (70-72)
5. CV mistakes & rules,
• Barrier Synchronization (73-75), Maybe 91
Checkin with 2 condition variables
self.allCheckedIn = Condition(self.lock)
self.allLeaving = Condition(self.lock)

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!

Implementing barriers is not easy.


92
Solution here uses a “double-turnstile”

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy