Project 1: Synchronization

due date April 27 (midnight at the end of April 27)

In this project, you will implement and use synchronization primitives. Turn-in instructions can be found here. Overall grading and extension policy can be found here. Demos will be needed for this project. Demos should be scheduled within one week of the project due date. To do this project, you need to understand (but not modify) the Nachos thread primitives. A description of the Nachos thread mechanism can be found here. Have questions? Send mail to Rohit Grover (grover@cs.ucsb.edu).
Most of the code we shall be dealing with in this project is in the nachos/code/threads directory. Unless stated otherwise, all files refered to in this project description are in (or are to be created in) this directory.

The files for this project are:

main.cc, threadtest.cc - a simple test of our thread routines.

thread.{h,cc} -- thread data structures and thread operations such as Fork(), Sleep(), Finish().
scheduler.{h,cc} -- manages the list of threads that are ready to run.
synch.{h,cc} -- synchronization routines: semaphores, locks, condition variables.
list.{h,cc} -- the list data structure.
synchlist.{h,cc} -- synchronized access to lists using locks and condition variables.
system.{h,cc} -- Nachos startup/shutdown routines.
switch.{h,s} -- assembly language code for starting up threads and context switching between them.
nachos/machine/interrupt.{h,cc} -- manage enabling and disabling interrupts.

You shall not be required to delete any code in nachos. You shall be adding code to it. Any additions which you make for this project should be encapsulated within

#ifdef SYNCH

...
...
#endif

The DEFINES variable in the Makefile should be modified as:

DEFINES = -DTHREADS -DSYNCH

The intention is that if you choose to not specify the -DSYNCH flag in the makefile, nachos should behave as it did when you first copied it.

Remember: Properly synchronized code should work no matter what order the scheduler chooses to run the threads on the ready list. In other words, we should be able to put a call to Thread::Yield() anywhere in your code, where interrupts are enabled, without changing the correctness of your code. To aid you in this, code linked with nachos will cause Thread::Yield() to be called on your behalf in a repeatable but unpredictatble way. Nachos code is repeatable in that if you call it repeatedly with the same arguments, it will do exactly the same thing each time. However, if you invoke nachos -rs #, with a different number each time, calls to Thread::Yield() will be inserted at different places in the code. Make sure that your code can handle context switches in an unpredictable manner (not just when Thread::Yield() is called explicitly). Further, it would also help a lot if you get familiarized with other command line options of nachos. A useful description of those options is provided in main.cc.


Problem 1

Run the program nachos in the threads directory for a simple test of the code in threadtest.cc. Trace the execution path (by hand). We want you to provide us the calling sequence of Nachos procedures for each thread (starting from the main thread) including how and when each thread was created. We also want you to understand what goes on when Thread::Yield is invoked by any thread. You are required to trace the context switch mechanism of threads (we do not expect you to figure out what goes on within the method `SWITCH'). At this point, it would be okay for you to understand that when one thread calls SWITCH, another thread starts running. For each procedure in the calling sequence, you should provide the name of the procedure and a description of all its arguments. Write up the execution trace in a file called TRACE.

Problem 2

Implement locks and condition variables using semaphores as a building block. The public interface to locks and condition variables is provided in synch.h. For this problem, you are to implement these interfaces (you may need to define private data for these classes). Note that it should not take much code to implement either locks or condition variables. An implementation of the semaphore primitive is provided in synch.cc. Read this implementation and make sure you understand the mechanics. You are to add your code to synch.cc. Remember to encapsulate your code within the required #ifdef and #endif statements so that you can choose to not compile your additions. Note that it is legal for a single thread to try to acquire a lock more than once. But, if any thread chooses to do so, it has to release the lock as many times as it tried to acquire it. We will test your implementations of locks and condition variables using programs that we have written. Be sure to test your code extensively (probably by using it in the subsequent problems). Your code must work for programs run using nachos -rs -- that is, your code must be able to handle context switches occurring in an unpredictable manner.

Problem 3

Consider the following program:


#include "system.h"
#include "synch.h"

int value;

void Increment(int delta)
{
  int i=0;

  int read_value;
  int write_value;

  for (;i<5;i++)
    {
      
      read_value = value;
      printf("%s read value:%d\n",currentThread->getName(),read_value);
      write_value = read_value + delta;

      currentThread->Yield();

      value = write_value;
      printf("%s wrote value:%d\n",currentThread->getName(),write_value);
      currentThread->Yield();
    }

  printf("%s saw final value:%d\n",currentThread->getName(),value);
}

void ThreadTest()
{
    value=0;

    Thread *t1 = new Thread("thread 1");
    Thread *t2 = new Thread("thread 2");
    Thread *t3 = new Thread("thread 3");

    t1->Fork(Increment, 1);
    t2->Fork(Increment, 2);
    t3->Fork(Increment, 3);
}

This program creates three threads. Each of these threads tries to increment a shared variable by a fixed amount multiple times. To demonstrate the problem of unsynchronized accesses to shared variables, we have split the increment operation and have deliberately embedded two calls Thread::Yield() within this operation. Remember, the correctness of concurrent execution of multiple threads should be independent of how the scheduler switches among them. Therefore, a well written piece of code accessing shared data should still work correctly in spite of unpredictable context switches. Replace the contents of threadtest.cc by the code above and compile it. If you run Nachos now, the output shows that the threads do not properly synchronize their access to the shared data. Using the synchronization primitives developed by you (or those provided by Nachos), correct the program. Remember, you are not allowed to remove the Thread::Yield() calls. Your synchronized code should work correctly in spite of the presence of the Yield calls. Copy your working version of the code into TSYNCH.cc. Note that anything left in threadtest.cc shall be ignored for grading purposes.

Problem 4

Consider the following program:



#include "copyright.h"
#include "system.h"
#include "synch.h"

#define STORAGE 5
#define MAXVAL 10


int array[STORAGE];
int read_head=0, write_head=0;
Semaphore *s;
int consumer_has_work=0;

void Produce(int dummy)
{
  static int data=1;
  int i=0;

  s->P();

  for (;i < MAXVAL;i++)
    {
      while ((write_head==read_head) && consumer_has_work)
	  currentThread->Yield();

      DEBUG('p',"in producer: read head:%d write head: %d\n",read_head,
	    write_head);

      array[write_head]=data++;
      write_head= (write_head+1)%STORAGE;
      
      consumer_has_work=1;
    }
  s->V();

}

void Consume(int dummy)
{

  int data;

  s->P();

  while (1)
    {
      while (consumer_has_work)
	{
	  DEBUG('p',"in consumer: read head:%d write head: %d\n",read_head,
		write_head);
	  
	  data=array[read_head];
	  read_head=(read_head+1)%STORAGE;
	  
	  printf("output:%d\n",data*data);
	  
	  if (data==MAXVAL)
	    return;

	  if (read_head==write_head)
	    consumer_has_work=0;
	}

      currentThread->Yield();
    }
  s->V();
}  
  
void ThreadTest()
{
    DEBUG('t', "Entering SimpleTest");

    Thread *t1 = new Thread("forked thread 1");
    Thread *t2 = new Thread("forked thread 2");

    s=new Semaphore("mutex",1);

    t2->Fork(Consume, 2);
    t1->Fork(Produce, 1);
}

It is an attempt to implement the "producer-consumer" problem. One thread is trying to produce data which is to be consumed by another thread. Try to read the code and figure out what the intentions of the programmer are. Replace the contents of threadtest.cc by this program, compile and run it. For some reason, this program fails to generate any output. You are required to correct this code so that it behaves in the expected producer-consumer fashion. Note that, this program uses the DEBUG facility of nachos. Read the file main.cc to find out how it is used using the '-d' option. NOTE: you are not allowed to delete any lines in this code. You are also not allowed to change the logic of this implementation of the producer-consumer problem.

Part a:

This code perhaps fails due to synchronization problems. You are required to correct this code by adding synchronization operations. Note that you do not need to add new synchronization variables (semaphores/locks/condition variables). The single semaphore currently being used is enough. You must not add more semaphores/locks/condition variables. Copy your working version of the code into PCSEM.cc. You also need to provide an explanation for all the changes you made to the given code. You can do this in the file PCREADME. Note that anything left in threadtest.cc shall be ignored for grading purposes.

Part b:

Use your implementation of locks from problem 2 to replace use of semaphores in part 4(a) above. This version of the code is to be left in PCLOCK.cc.

Problem 5

Consider a 3-digit counter which is meant to count from zero up to 150 (once it gets to 150, its gets stuck). Let us design and implement this counter using threads. Suppose each digit of the counter is kept in a separate variable and each such variable is manipulated by its own dedicated thread. Therefore we will need three threads for the three digits. We would like this counter to be incremented at 'clock ticks'. This means that we need another thread which notifies the other threads to represent each clock tick (for the time being, let us not bother about the relationship between these 'clock ticks' and physical time). One possible way to do this would be to let the 'clock-tick-thread' notify the thread for the least significant digit at each clock-tick. The thread for the least-significant-digit would increment the least significant digit by one or set it to zero as appropriate. If the least significant digit is set to zero, then its thread would notify the thread for the next digit. This process is repeated as necessary. Since we are not worried about the relationship between these 'clock ticks' and physical time, we can simulate the clock tick delay by an empty loop running for a large number of times. The overall design, then, is that the clock thread periodically notifies the thread for the least significant digit which then propagates this notification as and when appropriate. The thread corresponding to each digit is responsible for incrementing or resetting the digit and propagating the notification whenever the digit is reset. Note that this is similar to the operation of a ripple-carry adder. After each clock tick, the clock thread must print out the individual digits. Your implementation must not use any representation of the three-digit number being held in the counter other than the three variables being used to hold the individual digits. Implement this scheme in nachos using semaphores. Place your code in Counter.cc.

Problem 6

Five people are voting on a sequence of motions. A sixth person (the monitor) announces a motion and keeps track of the votes. As soon as the monitor announces a motion, all five of them rush to vote. Only one person can vote at a time. Each person can either vote FOR or AGAINST the motion. Each person makes her decision independently. As soon as there is a majority, the vote on the motion is closed and the monitor announces the result. The remaining members of the group refrain from voting once a vote is closed. The monitor then announces the next motion and the process continues as above. Model this scenario using six threads -- one thread for each voting member of the group and one thread for the monitor. You must use condition variables and locks to synchronize these threads. At any given time, only one thread should vote. As a part of voting, the thread should print its vote. As soon as a majority is reached, the monitor prints the result of the vote. Each motion announced by the monitor should have a unique identifier. The monitor must print this identifier as a part of announcing the motion and before any voting takes place. For this problem, your monitor thread should announce six motions (identifiers 1..6). Remember that if the fate of a motion becomes clear before all the 5 members have voted, the remaining members must not vote. You are to turn-in two different versions of the solution. These are to differ only in the voting policy -- that is how each thread votes on the motions.

Part a: the votes are static and each thread votes the same way independent of the motion. Assign the following names to the threads: "1", "2", "3", "4", "5", "6". The threads with odd numbered identifiers vote FOR the motion and threads with even numbered identifiers vote AGAINST the motion.

Part b: each thread votes randomly on each motion. You can use rand() to obtain a random integer ('man rand' will tell you more).

The code for these versions is to be left in VOTE1.cc and VOTE2.cc respectively. Your code must work when run using nachos -rs.