You are on page 1of 31

Monitor Solutions to Classical

Problems

Announcements
CS 415 Projects graded.
Mean 80.7, High 90 out of 90

CS 414 Homework due Monday.

Goals for Today


Continue with Synchronization Abstractions
Monitors and condition variables

Producer Consumer with Monitors


Readers-Writers problem and solution with Monitors

Review: Monitors
Monitors represent the logic of the program
Wait if necessary
Signal when change something so any waiting threads can
proceed

Basic structure of monitor-based program:


lock
while (need to wait) {
unlock
condvar.wait();
lock
}

Check and/or update


state variables
Wait if necessary

do something so no need to wait


condvar.signal();
unlock

Check and/or update


state variables
4

Review: Producer-consumer with


a bounded buffer

Problem Definition

Producer puts things into a shared buffer (wait if full)


Consumer takes them out (wait if empty)
Use a fixed-size buffer between them to avoid lockstep
Need to synchronize access to this buffer

Correctness Constraints:
Consumer must wait for producer to fill buffers, if none full
scheduling constraint

Producer must wait for consumer to empty buffers, if all full


scheduling constraint

Only one thread can manipulate buffer queue at a time


mutual exclusion

Remember why we need mutual exclusion


Because computers are stupid

General rule of thumb:

Use a separate semaphore for each constraint


Semaphore not_empty; // consumers constraint
Semaphore not_full; // producers constraint
Semaphore mutex; // mutual exclusion

Producer-Consumer using Semaphores


Init: Semaphore mutex = 1; /* for mutual exclusion*/
Semaphore not_full = N; /* number empty buf entries */
Semaphore not_empty = 0;
/* number full buf entries */
any_t buf[N];
int tail = 0, head = 0;

Producer

Consumer

void put(char ch) {

char get() {

P(not_full);
P(mutex);

P(not_empty);
P(mutex);

// add ch to buffer
buf[head%N] = ch;
head++;

// remove ch from buffer


ch = buf[tail%N];
tail++;

V(mutex);
V(not_empty);

V(mutex);
V(not_full);
}

return ch;

Discussion about Bounded Buffer


Solution
Why asymmetry?
Producer does: P(not_full), V(not_empty)
Consumer does: P(not_empty), V(not_full)

Is order of Ps important?
Yes! Can cause deadlock

Is order of Vs important?
No, except that it might affect scheduling efficiency

What if we have 2 producers or 2 consumers?


Do we need to change anything?

Review: Motivation for Monitors and


Condition Variables
Semaphores are a huge step up, but:
They are confusing because they are dual purpose:
Both mutual exclusion and scheduling constraints
Example: the fact that flipping of Ps in bounded buffer gives deadlock is
not immediately obvious

Cleaner idea: Use locks for mutual exclusion and condition variables
for scheduling constraints

Definition: Monitor: a lock and zero or more condition


variables for managing concurrent access to shared data
Use of Monitors is a programming paradigm
Some languages like Java provide monitors in the language

The lock provides mutual exclusion to shared data:


Always acquire before accessing shared data structure
Always release after finishing with shared data
Lock initially free
8

Condition Variables
How do we change the get() routine to wait until something is
in buffer?
Could do this by keeping a count of the number of things on the
queue (with semaphores), but error prone

Condition Variable: a queue of threads waiting for something


inside a critical section
Key idea: allow sleeping inside critical section by atomically releasing
lock at time we go to sleep
Contrast to semaphores: Cant wait inside critical section

Operations:
Wait(&lock): Atomically release lock and go to sleep. Re-acquire
lock later, before returning.
Signal(): Wake up one waiter, if any
Broadcast(): Wake up all waiters

Rule: Must hold lock when doing condition variable ops!


9

Producer Consumer using Monitors


Monitor Producer_Consumer {
any_t buf[N];
int n = 0, tail = 0, head = 0;
condition not_empty, not_full;
void put(char ch) {
while(n == N)
wait(not_full);
buf[head%N] = ch;
head++;
n++;
signal(not_empty);
}

char get() {
while(n == 0)
wait(not_empty);
ch = buf[tail%N];
tail++;
n--;
signal(not_full);
return ch;
}

10

Reminders: Subtle aspects


Notice that when a thread calls wait(), if it blocks it also
automatically releases the monitors mutual exclusion
lock
This is an elegant solution to an issue seen with
semaphores
Caller has mutual exclusion and wants to call P(not_empty)
but this call might block
If we just do the call, the solution deadlocks
But if we first call V(mutex), we get a race condition!

11

Review: Mesa vs. Hoare monitors


Need to be careful about precise definition of signal and wait.
Consider a piece of our dequeue code:
while (n==0) {
wait(not_empty); // If nothing, sleep
}
ch = buf[tail%N]; // Get next item
Why didnt we do this?
if (n==0) {
wait(not_empty); // If nothing, sleep
}
ch = buf[tail%N]; // Get next item

Answer: depends on the type of scheduling


Hoare-style (most textbooks):

Signaler gives lock, CPU to waiter; waiter runs immediately


Waiter gives up lock, processor back to signaler when it exits critical
section or if it waits again

Mesa-style (Java, most real operating systems):


Signaler keeps lock and processor
Waiter placed on ready queue with no special priority
Practically, need to check condition again after wait

12

Review: Can we construct Monitors


from Semaphores?
Locking aspect is easy: Just use a mutex
Can we implement condition variables this way?
Wait()
{ P(x_sem); }
Signal() { V(x_sem); }

Doesnt work: Wait() may sleep with lock held

Does this work better?


Wait() {
V(mutex);
// Release mutex lock
P(x_sem);
P(mutex);
// Acquire mutex lock
}
Signal() { V(x_sem); }

No: Condition vars have no history, semaphores have history:

What if thread signals and no one is waiting? NO-OP


What if thread later waits? Thread Waits
What if thread Vs and noone is waiting? Increment
What if thread later does P? Decrement and continue
13

Construction of Monitors from


Semaphores (cont)

Problem with previous try:

P and V are commutative result is the same no matter what order


they occur
Condition variables are NOT commutative

Does this fix the problem?


Wait(Lock lock) {
V(mutex);
// Release mutex lock
P(x_sem);
P(mutex);
// Acquire mutex lock
}
Signal() {
if semaphore queue is not empty
V(x_sem);
}

Not legal to look at contents of semaphore queue


There is a race condition signaler can slip in after lock release and
before waiter executes semaphore.P()

It is actually possible to do this correctly


Complex solution for Hoare scheduling in book
Can you come up with simpler Mesa-scheduled solution?

14

Construction of Hoare Monitors


using Semaphores
Wait(){
x_count++;
if(next_count>0)
V(next);
else
V(mutex);
P(x_sem);
x_count;
Signal(){
If(x_count>0){
next_count++;
V(x_sem);
P(next);
next_count;
}

For each procedure F:


P(mutex);
/* body of F */
if(next_count > 0)
V(next);
else
V(mutex);

15

Revisits: Readers/Writers Problem


W
R

R
R

Motivation: Consider a shared database


Two classes of users:
Readers never modify database
Writers read and modify database

Is using a single lock on the whole database sufficient?


Like to have many readers at the same time
Only one writer at a time

16

Revisit: Readers/Writers Problem


Correctness Constraints:

Readers can access database when no writers


Writers can access database when no readers or writers
Only one thread manipulates state variables at a time

Basic structure of a solution:


Reader()

Wait until no writers


Access data base
Check out wake up a waiting writer

Writer()

Wait until no active readers or writers


Access database
Check out wake up waiting readers or writer

State variables (Protected by a lock called lock):

int NReaders: Number of active readers; initially = 0


int WaitingReaders: Number of waiting readers; initially = 0
int NWriters: Number of active writers; initially = 0
int WaitingWriters: Number of waiting writers; initially = 0
Condition canRead = NIL
Conditioin canWrite = NIL

17

Revisit: Readers-Writers Problem


One issue we need to settle, to clarify problem statement.
Suppose that a writer is active and a mixture of readers and
writers now shows up. Who should get in next?
Or suppose that a writer is waiting and an endless of stream of
readers keeps showing up. Is it fair for them to become active?

Well favor a kind of back-and-forth form of fairness:


Once a reader is waiting, readers will get in next.
If a writer is waiting, one writer will get in next.

18

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

19

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

20

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

21

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

22

Understanding the Solution


A writer can enter if there are no other active writers and
no readers are waiting

23

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

24

Understanding the Solution


A reader can enter if
There are no writers active or waiting

So we can have many readers active all at once


Otherwise, a reader waits (maybe many do)

25

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

26

Understanding the Solution


When a writer finishes, it checks to see if any readers
are waiting
If so, it lets one of them enter
That one will let the next one enter, etc

Similarly, when a reader finishes, if it was the last reader,


it lets a writer in (if any is there)

27

Readers and Writers


Monitor ReadersNWriters {
int WaitingWriters, WaitingReaders,NReaders, NWriters;
Condition CanRead, CanWrite;
Void BeginWrite()
{
if(NWriters == 1 || NReaders > 0)
{
++WaitingWriters;
wait(CanWrite);
--WaitingWriters;
}
NWriters = 1;
}
Void EndWrite()
{
NWriters = 0;
if(WaitingReaders)
Signal(CanRead);
else
Signal(CanWrite);
}

Void BeginRead()
{
if(NWriters == 1 || WaitingWriters > 0)
{
++WaitingReaders;
Wait(CanRead);
--WaitingReaders;
}
++NReaders;
Signal(CanRead);
}
Void EndRead()
{
if(--NReaders == 0)
Signal(CanWrite);
}

28

Understanding the Solution


It wants to be fair
If a writer is waiting, readers queue up
If a reader (or another writer) is active or waiting, writers queue
up
this is mostly fair, although once it lets a reader in, it lets ALL
waiting readers in all at once, even if some showed up after
other waiting writers

29

Subtle aspects?
The code is simplified because we know there can only
be one writer at a time
It also takes advantage of the fact that signal is a no-op if
nobody is waiting
Where do we see these ideas used?
In the EndWrite code (it signals CanWrite without checking for
waiting writers)
In the EndRead code (same thing)
In StartRead (signals CanRead at the end)

30

Comparison with Semaphores


With semaphores we never did have a fair solution of
this sort
In fact it can be done, but the code is quite tricky

Here the straightforward solution works in the desired


way!
Monitors are less error-prone and also easier to understand
C# and Java primitives should typically be used in this manner,
too

31

You might also like