Lecture 9 - Semaphores

(Review): lwp_create()

It has to:

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:

#define N 100
int count = 0;
producer:
	for(;;)
	{
		produce();
		if(count == N)
			sleep();
		insert_item();
		count++;
		if(count == 1)
			wakeup(consumer);
	}

consumer:
	while(true)
	{
		if(count == 0)
			sleep();
		remove_item();
		count--;
		if(count == N-1)
			wakeup(producer);
		consume();
	}

But this didn't work, as we talked about last time. In short we need semaphores.

semaphores (Dijkstra, 1967)

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:

monitors (Hoare 1974, Brinch-Housen 1975)

A region of a program where only one thread may be active at a time.

These communicate something called the condition variables:

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:

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: