All your work for this assignment will take place within the
threads
directory.
Although you might find it helpful to look
at files outside this directory, you should not make any
changes to files other than those in the threads
directory.
In general, if a source code file has the following message at the
beginning:
In this assignment, and in all subsequent assignments in this course, you will be given very detailed specifications concerning particular functions or classes you are to implement. Though you will generally have considerable flexibility in exactly how you implement these functions, it is extremely important that the names of these functions and their behaviors are exactly as specified. If an assignment tells you to implement a function or class with a particular name and/or prototype, do not implement something with a different name or prototype.
In this course, we are going to perform a great deal of the building and testing of your software automatically. We will write our own driver functions which we will link with your code. If you adhere exactly to the specifications, everything should work properly. If not, your code will require manual intervention to test, and the large number of students in the class will probably make this impossible for us.
One other thing that will make manual intervention necessary is if your code produces extraneous printout when it runs. You should therefore see to it that your code, as submitted, should produce no printout other than that originally present in Nachos, or that specifically indicated in the assignment. In addition, if the assignment specifies that you are to produce printout, your printout must appear exactly as specified. Do not add extraneous newlines, extra characters, or change in any way the format of the messages you are told to produce.
You will probably find it useful to incorporate debugging printout into your code during development and testing. This is OK, but all such code should use the NACHOSDEBUG
macro defined in threads/utility.h, so that
debugging printout is not produced unless a suitable command-line flag
is provided. Examples of the use of this macro appear throughout the
NACHOS code. Every time you modify a NACHOS file, you should surround all of your changes with preprocessor commands that make it easy to compile the code without your changes.
When the ``threads'' version of NACHOS is started, it initially
creates a single thread that begins executing the function
ThreadTest()
in the file threadtest.cc. This function
creates a new thread that calls the function
SimpleThread(1)
, and the original thread calls the
function SimpleThread(0)
. The two threads each execute
the loop in the SimpleThread()
function, in which they
yield control of the CPU back and forth five times.
ThreadTest()
to take a single integer
argument n
, so that its prototype becomes:
ThreadTest()
should fork
n
new threads instead of just one.
Test your function on values of n
from zero to four.
You will have to change the file
main.cc
to supply
the required integer argument to ThreadTest()
.
Note that the total number of running threads is n+1
, which includes
the main thread and n
forked threads.
SimpleThread()
to read
exactly as follows:
ThreadTest()
set to 0, 1, 2, 3, and 4.
Analyze and explain the results.
Put your explanation in the file threads/HW1_WRITEUP
.
Note that the total number of running threads is n+1
.
Each loop iteration increments SharedVariable by 1 and one would expect the final value to be (n+1)*5
when the initial value of SharedVariable is 0. You need to explain why this final value is not seen.
Semaphore::P()
and Semaphore::V()
.
Modify SimpleThread()
by introducing calls to the semaphore
operations, so that accesses to the shared variable are properly synchronized.
Try your synchronized version of SimpleThread()
with the
argument to ThreadTest()
set to 0, 1, 2, 3, and 4.
Access to the shared variables are properly synchronized if each
iteration of the loop increments the variable by exactly one and each
thread sees the same final value.
Note that you will first need to identify the critical section within
the loop iteration in Because every thread needs to print the same final value, you would need to implement a mechanism so that
printing the final value will not be executed until all threads complete the update of SimpleThread()
and then add the semaphore synchronization before and after the critical
section. The final value to be seen needs to be (n+1)*5
with total n+1
running threads.
Your synchronized code should work when Yield() calls are inserted in any location within the loop or
when they are removed.
SharedVariable
.
Using the #ifdef
/#endif
preprocessor construct,
bracket your synchronization code so that its compilation is controlled by
the preprocessor symbol HW1_SEMAPHORES
.
That is, if the preprocessor symbol HW1_SEMAPHORES
is defined,
the synchronization code should be included, otherwise the synchronization
code should not be included.
Recompile NACHOS both ways to make sure our "conditionalized" code works
properly.
Lock
operations that are missing from the file
synch.cc.
Locks are used to ensure mutual exclusion between threads.
At any time, a lock is either free, or else it is held
by a thread. At most one thread at a time can hold a lock.
If a thread tries to acquire a lock that is currently being held
by another thread, the requesting thread will block (sleep)
until the thread that holds the lock releases the lock. There are four missing functions you must implement:
Lock::Lock(char* debugName)
:
This constructor function initializes a lock object.
The debugName
argument is a string supplied by the
caller, which should just be stored into the new Lock
structure. Its purpose is simply to help distinguish various instances
of locks in debugging printout.
Lock::~Lock()
:
This function deallocates a lock object, when it is no longer needed.
void Lock::Acquire()
:
This function waits for a lock to become free and then acquires
the lock for the current thread.
void Lock::Release()
:
This function releases a lock that was previously acquired by the
current thread, and wakes up one of the threads
waiting for the lock.
Semaphore
code as a
guide when writing the Lock
code. Locks are very much
like semaphores with initial value 1.
Test your code by replacing the Semaphore
operations you
added to SimpleThread()
by corresponding
Lock
operations, and verifying that the demonstration
still works properly. Conditionalize your code so that its
complication is controlled by the preprocessor symbol
HW1_LOCKS
. (Be sure that you can still compile your code
with HW1_SEMAPHORES
defined to get the semaphore
demonstration.)
Condition
operations that are missing from the file
synch.cc.
Conditions are used to ensure proper synchronization among threads.
The specifications for this primitive appear in the synch.h
module.
There are five missing functions you must implement:
Condition::Condition(char* debugName)
:
This constructor function initializes a condition object.
The debugName
argument is a string supplied by the
caller, which should just be stored into the new Condition
structure. Its purpose is simply to help distinguish various instances
of conditions in debugging printout.
Condition::~Condition()
:
This function deallocates a condition object, when it is no longer needed.
void Condition::Wait(Lock* conditionLock)
:
This function waits for a condition to become free and then acquires
the condition for the current thread.
void Condition::Signal(Lock* conditionLock)
:
This function wakes up one of the threads that is waiting on the condition.
void Condition::Broadcast(Lock* conditionLock)
:
This function wakes up all threads that are waiting for the condition.
Semaphore
and Lock
code as a
guide when writing the Condition
code. Task 4 will use locks and conditions to synchronize the laundromat application code and thus Task 4 can serve as test cases for the condition implementation.
available[NMACHINES]
whose elements
are non-zero if the corresponding machines are available (NMACHINES
is a constant indicating how many machines there are in the laundromat), and a
semaphore nfree
that indicates how many machines are available.
The code to allocate and release machines is as follows:
available
array is initialized to all ones, and nfree
is initialized to NMACHINES.
threads/HW1_WRITEUP
.
Note that during the functionality test, you can fork a few threads (e.g. 2, 3) running as client stations to allocate and release washing machines periodically. The number of available washing machines to be tested can be ranged from 2 to 10. You need to design and print some information to demonstrate these stations run smoothly without race conditions.
When you submit your assignment, make sure that all preprocessor
directives (HW1_SEMAPHORES, HW1_LOCKS,
HW1_LAUNDRY_SEM, HW1_LAUNDRY_LC
) are disabled. That is, your
submission should simply output the Nachos default. We will
automatically build your submission with different preprocessor
directives. In order to do that, it is necessary that you leave
the $(DEFINES)
variable in the compiler invocation in
Makefile.common
. You can add directives to the
compilation process as you like, but we will use the
DEFINES
variable to pass our defines to the
compiler. Note that you do not need to do anything right
away. Only when you substantially change the Makefile(s), it is
necessary for you to make sure that the DEFINES
variable can still be used to pass arguments to the compilation
process.
You have to provide a writeup in a file
HW1_WRITEUP
and 3 test trace files.
These files should be in the code/threads directory. This HW1_WRITEUP file should contains
TA will recompile using your makefile instruction and test.
HW1_Task1
, HW1_Taks2
, HW1_Task4
,
illustrating your code works.
Please include a running trace of Task 1 (with n=4), Task 2 (with n=4), and Task 4 respectively.
These files are useful for TAs to compare test results.
Once the above writeup is done,
You can turnin up to 3 times per project and not more than that!
The earlier versions will be discarded.
Note: only one turnin per group is accepted!