Lecture 9 - Semaphores
(Review): lwp_create()
It has to:
- Allocate a
thread_context
mmap
the stack- Use
swap_rfiles
to swap to some new thread.
Keep in mind that swap_rfiles
sets the registers, doesn't save them (unless the correct NULL
is provided).
What you load into %rsp
doesn't matter much. What you load into %rbp
is very important, as it's your new stack pointer.
More on Synchronization - Semaphores
Recall our producer/consumer solution from last time:
But this didn't work, as we talked about last time. In short we need semaphores.
It is a counter with two atomic operations:
P(s) = DOWN(s);
if(s == 0)
sleep();
else
s--;
V(s) = UP(s);
if(exists sleeping proc)
wake one;
else
s++;
Notice that you have to have the operations be atomic! We have to do something to make it this way. Otherwise they are equivalent approaches.
So how do we fix this?
Using More Semaphores to Fix the Semaphore
We'll use 3 semaphores:
#define DOWN(s) (s = s--)
#define UP(s) (s = s++)
// ...
semaphore_t full = 0; // number of full cells in buffer
semaphore_t empty = N; // number of empty cells in the buffer
semaphore_t mutex = 1; // boolean lock, whether something is using it
producer(){
for(;;){
produce();
DOWN(empty); // wait for it to empty
DOWN(mutex); // get access
enter_item();
UP(mutex); // unlock mutex
UP(full); // wait until it is full
}
}
consumer(){
for(;;){
DOWN(full); // we need a full one, so wait
DOWN(mutex);
remove_item();
UP(mutex); // allow mutex locking
UP(empty); // wait until it's empty
}
}
The idea here is that it is as restrictive as possible. It definitely works; however, it's harder to get right than it looks. For example, what if we flip the first two locks in each:
// ... in consumer()
DOWN(mutex); // unlock the mutex.
DOWN(full); // uh oh! Now the consumer is waiting for the producr to put an item in, but it can't since it is waiting on the DOWN(mutex) line.
// ...
Language Support
We can do something similar and add monitors to our code:
A region of a program where only one thread may be active at a time.
These communicate something called the condition variables:
wait(cond)
: wait untilsignal(cond)
is send to your threadsignal(cond)
: signal a specific thread.
It's going to use a semaphore to do this. Your compiler will add these semaphores in when synchronization is needed in there. Let's make one:
// Monitor Producer-Consumer
condition_t full, empty;
unsigned int count = 0;
// here `procedure` is saying that there's no return type. It replaces the code inline, but all as one procedure.
procedure_t enter(){
if (count = N)
wait(full);
enter_item(); // don't need a mutex since only one thing is active here, and we are in it
count = count + 1;
if (count = 1)
signal(empty);
}
procedure_t remove(){
if (count = 0)
wait(empty);
remove_item();
count = count - 1; //like above don't need a mutex
if (count = N)
signal(full);
}
producer(){
while(true)
{
produce();
enter();
}
}
consumer(){
while(true)
{
consume();
remove();
}
}
C itself doesn't have this support. But a nicer language would have this support.
Message/Signal Passing
We have two primitives:
send(dst, msg)
recieve(src, msg)
We have some decisions for send()
. Do we wait for there to be a receive()
synchronously? Or do we just do it asynchronously and hope for the best?
Some considerations:
- buffering: you may have to put the message in the same buffer
- copying: long message take lots of time to resend
- transmission time: if it takes so long to send the message, it might be better to do it yourself!