Menu

Class 18: Concurrency III


Reading
The Little Book of Semaphores is a great book about synchronization and semaphores, you can download the pdf.

Homework
Printout the Homework and answer the questions on that paper.

Synchronization
Normally distinct threads or processes proceed asynchronously, meaning the relative speeds at which they execute and the order in which operations from one thread/process versus another actually occur is completely unconstrained. From one run to another, any relative ordering of the instructions of separate threads/processes might occur.

Enforcing ordering constraints between instructions of different threads/processes is called syncronization. Mutual exclusion of critical sections is just one kind of syncronization. Many common synchronization patterns have been identified, including:

Synchronization primitives
One of the biggest overarching themese in computing is separation of interface from implementation. For example, the prototype double cos(double x); is the interface to the cosine function in math.h. As programmers, we only care about that interface, not the implementation. In OOP the interface for a class is its public member functions. These are the operations we as outside programmers can use. All the private data data and member functions ... those don't matter to use outsiders. In data structures, which you'll have next semester, this gets taken much farther still. Finally, all these system calls that we've been learning about: they are our (as programmers) interface to the kernel. Prototypes and man page documentation tell us what these functions accomplish, but how they are implemented is a completely different story --- a story that we don't really have to know to be able to use them.

Finding the right interface is very important, and often quite difficult. Ideally, you want to provide a few simple operations that are easy to understand, but can be combined to accomplish a wide variety of complex tasks. A small set of operations like this is often called a set of primitive operations. We will study semaphores, which provide a small set of primitive operations for synchronizing threads or processes that can be combined to provide many different synchronization patterns, including the four listed above.

Semaphores
A semaphore is essentially an integer counter with two operations: post, which increments the semaphore, and wait, which decrements it. The important thing is that if a wait is performed and the semaphore value is not positive, the thread performing the wait is suspended until the value of the semaphore is positive. When a post operation takes a semaphore's value from zero to positive and there are suspended threads waiting on the semaphore, one of the suspended threads is awakened, and its wait operation proceeds succesfully.

Semaphores are probably the best known synchronization primitive. Mutexes are nothing more than a semaphore with initial value one, with every process doing a wait immediately prior to a critical seciton and a post immediately after.

Synchronization with semaphores
Semaphores trivially give us mutex's. Try to figure out how to use them to do signalling and rendevous from above.

Producer/consumer (infinite buffer) with semaphores
Let's consider the producer consumer problem with one producer, one consumer, and an infinite buffer for data. We'll require that we don't have both producer and consumer accessing the buffer simultaneously. Aside from that mutual exclusion requirement, we need to ensure that the consumer never tries to read from an empty buffer, i.e. never gets ahead of the producer.

Of course we have mutex's for mutual exclusion, but a mutex is no different than a semaphore with initial value 1. So we'll use a semaphore named mu, initialized to 1, to ensure mutual exclusion. We'll have another semaphore named items whose value is the number of items currently stored in the buffer. Thus, every time the producer produces, items should be incremented. Every time the consumer consumes, items should be decremented, and since the consumre can't consume unless items > 0, the consumer will "wait" on items.

Shared semaphores mu initialized to 1, and items initialized to 0.
ProducerConsumer
while(1)
{
  get/compute data
  wait(mu);
  write data item to buffer
  post(items);
  post(mu);
}
		
while(1)
{
  wait(items);
  wait(mu);
  read data item form buffer
  post(mu);
  process data
}
	      

Hopefully you can convince yourself that this works properly.

Producer/consumer (finite buffer) with semaphores
In many scenarios we only have a finite buffer. (Well, in all situations we have finite buffers, but when they're GB's worth of space, we treat them as if they were infinite.) In this case, not only must the consumer thread sleep while the buffer's empty, but the producer thread must be put to sleep when the buffer is full. We can accomplish this by adding another semaphore, spaces, initialized to the capacity of the buffer, that keeps track of the number of empty slots in the buffer. Thus, BUFFERSIZE = #items + #spaces at all times.

Shared semaphores mu initialized to 1, and items initialized to 0, and spaces inialized to BUFFERSIZE.
ProducerConsumer
while(1)
{
  get/compute data
  wait(spaces);
  wait(mu);
  write data item to buffer
  post(items);
  post(mu);
}
		
while(1)
{
  wait(items);
  wait(mu);
  read data item form buffer
  post(spaces);
  post(mu);
  process data
}
	      

Hopefully you can convince yourself that this works properly as well.