#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.