Assignment 1. Threads


  • Initial design spec due on February 3, Tuesday, 11:59 PM.
  • Design reviews will happen on February 5.
  • Complete code and documentation due on February 11, Wednesday, 11:59 PM.
  • Read Information on using RCS before starting to hack. It also contains the detailed instructions on how to submit your assignments. Questions regarding rcssubmit should be sent to your TA.


    Introduction

    In this assignment, we give you part of a working thread system; your job is to complete it, and then to use it to solve several synchronization problems.

    The first step is to read and understand the partial thread system we have written for you. This thread system implements thread fork, thread completion, along with semaphores, locks, and conditional variables for synchronization. When you run the Nachos binary, it executes a function called ThreadTest() (located in threads/threadtest.cc). This function creates two threads that take turns using the machine by repeatedly calling currentThread->Yield().

    You should trace this execution path by hand to better understand how Nachos threads are implemented. When doing this, it is helpful to keep track of the state of each thread and which procedures are on each thread's execution stack. You will notice that when one thread calls SWITCH(), another thread starts running; the first thing the new thread does is to return from SWITCH(). This comment may seem cryptic to you at this point, but you will understand threads much better once you understand why the SWITCH() that gets called is different from the SWITCH() that returns. (Note: because gdb does not understand threads, you will get bizarre results if you try to trace across a call to SWITCH() in gdb.)

    The files for this assignment are:

  • main.cc, threadtest.cc --- a simple test of our thread routines.
  • thread.h, thread.cc --- thread data structures and thread operations such as thread fork, thread yield, thread sleep, and thread finish.
  • scheduler.h, scheduler.cc --- manages the list of threads that are ready to run.
  • synch.h, synch.cc --- synchronization routines: semaphores, locks, and condition variables.
  • list.h, list.cc --- generic list management (as done in LISP).
  • synchlist.h, synchlist.cc --- synchronized access to lists using locks and condition variables (useful as an example of the use of synchronization primitives).
  • system.h, system.cc --- Nachos startup/shutdown routines.
  • utility.h, utility.cc --- some useful definitions and debugging routines.
  • switch.h, switch.s --- assembly language magic for starting up threads and context switching between them.

  • interrupt.h, interrupt.cc (in the machine directory) --- manage enabling and disabling interrupts as part of the machine emulation.
  • timer.h, timer.cc (in the machine directory) --- emulate a clock that periodically causes an interrupt to occur.
  • stats.h, stats.cc (in the machine directory) --- collect interesting statistics.
  • 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 (causing the scheduler to choose another thread to run) anywhere in your code where interrupts are enabled without changing the correctness of your code. You will be asked to write properly synchronized code as part of the later assignments, so understanding how to do this is crucial to being able to do the project.

    To aid you in this, code linked in with Nachos will cause Thread::Yield to be called on your behalf in a repeatable but unpredictable 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 N'', with a different number N each time, calls to Thread::Yield will be inserted at different places in the code. This will change the sequence in which the threads execute. You can also use the -d option to print out the tick-by-tick thread information.

    Make sure to run various test cases against your solutions to these problems; for instance, for Problem 2, create multiple producers and consumers and demonstrate that the output can vary, within certain boundaries.

    Warning: in our implementation of threads, each thread is assigned a small, fixed-size execution stack. This may cause bizarre problems (such as segmentation faults at strange lines of code) if you declare large data structures to be automatic variables (e.g., "int buf[1000];"). You will probably not notice this during the semester, but if you do, you may change the size of the stack by modifying the StackSize define in switch.h.

    Although the solutions can be written as normal C routines, you will find organizing your code to be easier if you structure your code as C++ classes. Also, there should be no busy-waiting in any of your solutions to this assignment.


    Problem 1

    Implement condition variables (the Condition class) directly using interrupt enable and disable to provide atomicity. We have provided a sample implementation that uses semaphores; your job is to provide an equivalent implementation without using semaphores; once you are done, we will have two alternative implementations that provide the exact same functionality. You do not need to reimplement the Lock class; you can use the provided version.


    Problem 2

    Implement synchronous "send" and "receive" of one word messages (also known as Ada-style rendezvous), using condition variables. Create a Mailbox class with operations Mailbox::Send(int message) and Mailbox::Receive(int *message). Send atomically waits until Receive is called on the same Mailbox and then copies the message into the receive buffer. Once the copy is made, both can return. Similarly, the Receive waits until Send is called, at which point the copy is made, and both can return. Your solution should work even if there are multiple senders and receivers for the same Mailbox.

    You solution should make use of only condition variables and locks for synchronization. Do not use semaphores. If there are multiple senders and receivers on the same Mailbox, you may choose any ordering you wish, as long as your choice is clearly documented in your README file and no messages are lost.


    Problem 3

    Implement void Thread::Join(void) in Nachos. This routine causes the currently running thread to block until the joined thread has completed. Add an argument to the thread constructor that says whether or not a Join may be called on this thread (i.e., "Thread::Thread(char* threadName, bool joinMayBeCalled)"). Note that you do not need to implement Join so that it returns the value returned by the forked thread.

    It is legal for the parent thread to never call Thread::Join. Your solution should properly delete the thread control block whether or not Join is called, and whether or not the forked thread finishes before the Join is called. That is, the Thread object should be freed exactly once in all cases.

    The parent thread may call Join() on a child thread arbitrarily many times. Each one after the first is a "subsequent" join. (Having this functionality is convenient if Join() might be called along many different paths of execution.) The following code fragment is legal and must be handled correctly:

                    Thread *child = new Thread(...);
                    child->Fork(...);
                    child->Join();
                    ...
                    child->Join();  // doesn't do anything.
    
    Here, "subsequent" only refers to function calls on the same Thread object, not joins to a different child thread.

    Similarly, calling Join() on an "unjoinable" thread is a no-op.


    Problem 4

    Implement an "alarm clock" class. Threads call "Alarm:GoToSleepFor(int howLong)" to go to sleep for a period of time. The alarm clock can be implemented using the hardware Timer device (cf. timer.h). When the timer interrupt goes off, the Timer interrupt handler checks to see if any thread that had been asleep needs to wake up now. There is no requirement that threads start running immediately after waking up; just put them on the ready queue after they have waited for the approximately the right amount of time.


    Problem 5

    You've been hired by the University to build a controller for the elevator in the Watson Hall, using semaphores or condition variables. The elevator is represented as a thread; each student or faculty member is also represented by a thread. In addition to the elevator manager, you need to implement the routines called by the arriving student/faculty: ``ArrivingGoingFromTo(int atFloor, int toFloor)''. This should wake up the elevator, tell it which floor the person is on, and wait until the elevator arrives before telling it which floor to go to. The elevator is amazingly fast, but it is not instantaneous -- it takes only 100 ticks to go from one floor to the next. You may find it useful to use your solution for Problem 4 here. For simplicity, you can assume there's only one elevator, and that it holds an arbitrary number of people. You are allowed to give higher priority to those threads going to or departing from the Zoo!