CS 354 Lab 5: Thread Synchronization Primitives

Goal

In this lab, you will create a new Solaris-like synchronization primitive in NachOS called a read/write lock, and implement an MS Windows-like atomic global map that can be used to share data via (name,value) pairs between threads. The atomic map will also support synchronization of threads that access it.

Make sure to read Chapter 7 of the Silberschatz book carefully before you begin working on this lab.

Source

The source from which you should start is the same as the original source we used for lab 3: nachos.tar.gz
Instructions for extracting it can be found in the Lab 3 handout. Do NOT use a version of the code where Fork() has an extra parameter.

Assignment

Part 1

Create a new synchronization primitive class that follows the definition below:
   class RWLock
   {
   public:
		enum LockType {READER, WRITER};

		RWLock();

		~RWLock();

		void Lock(LockType OptionalType = READER);

		void Unlock(LockType OptionalType = READER);

    private:
	// your private data is declared here... You can use Semaphores 
        // and any other variables you need
    };
Based on the optional LockType (the C++ default arguments to Lock/Unlock give the value READER if the caller does not specify an argument), the Lock and Unlock functions can behave as read or as write locks.

The locks should behave in the same manner as in the readers/writers problem with reader priority. This means that any number of readers can gain access if another reader is holding the lock. When a writer holds the lock, no other reader or writer can gain access. When a writer finishes writing, waiting readers must have priority over waiting writers. This is similar to the standard readers/writers solution given in Chapter 7 of your Silberschatz book, except that readers have priority when a writer finishes writing. Clearly, writers may starve, but this is the specification you should follow.

The above class should be implemented in synch.h and synch.cc.

Part 2

Implement an atomic map -- shared among all threads -- where a name string is associated with a value (some number). Many times, it is useful to have such global atoms ((key, value) pairs) shared among threads. For instance, an entry in this global table could be used for message passing between threads or for sharing data. In addition to being able to share globally mapped atoms, the map can be used for synchronizing threads that access an entry.

Each atom has an owner that can perform certain operations that other threads cannot perform on that atom. The thread that creates an atom is its owner and is capable of changing its state and not just reading its value. The owner can delete an atom or block it. An atom in a blocked state will cause any readers (except the owner) to be placed on a wait queue until the owner thread performs the unblock operation, which will move all waiting threads to the ready list. If any threads are on a wait queue of some atom, they must be freed up when the thread calls unblock, deletes the atom, or terminates. Upon termination, all atoms that have been created by that thread must be removed from the atom table. All of the operations on the map and on an atom must be multi-thread safe. However, the read operations should not be restrictive when other read operations are occurring (therefore, use RWLock's).

The object of type AtomTable that you declare must be globally accessible -- just like the scheduler object (examine system.h and system.cc). Call the pointer that points to an object of type AtomTable "atomTbl". Make sure to use new to allocate space for the table in system.cc.

Your task is to implement the API functions listed below to allow the use of the atomic table. Since a computer has only a limited number of resources and OS operations need to be as fast as possible, the number of table entries is limited to 20 entries. Your code must handle the situation when an atom is deleted while some other thread is waiting to read it. The function that waited on an atom that got deleted needs to return ATOM_DELETED, even if some new atom with the same name as the deleted one has been reinserted into its old spot.

Define the following enumerated type as a public member of the class AtomTable:

		enum ErrCond {ATOM_OK, ATOM_FAIL, ATOM_DELETED};
The public members of the AtomTable class are defined as follows:
	class AtomTable
	{
	public:
		enum ErrCond {ATOM_OK, ATOM_FAIL, ATOM_DELETED};

		AtomTable();

		ErrCond InsertAtom(char *name, int value); // create a new atom

		ErrCond DeleteAtom(char *name);  // only the owner can call this

		ErrCond GetAtom(char *name, int *value); // read the value of an atom

		ErrCond TryGetAtom(char *name, int *value); // read the value of an atom if unblocked; return error otherwise

		ErrCond BlockAtom(char *name); // only the owner can call this

		ErrCond UnblockAtom(char *name); // only the owner can call this

		ErrCond DestroyAtoms(); // MUST be called by a thread upon exit

	private:
		// your private data and functions are declared here
                // in addition to the actual atom table (which
                // contains for each atom its name, value, and any
                // locks and semaphores required), declare any other
                // RWLocks and variables you need.
                // You can also declare private functions that are 
                // used from within public functions here.
	};
The AtomTable methods should implement the following functionality:

You should create a new pair of files atom.h and atom.cc where you will implement the atom table. Do not forget to change the Makefile so that the new files will be included into the build (Hint: see target "depend" in Makefile.common).

Testing

As before, you are responsible for making sure that all of your code works according to the specifications. We are providing you with a simple test case for RW locks at threadtest.cc. This test case only checks for violations among writers or among readers and writers, but you will need to modify it if you want to do more rigorous tests, e.g., to test your reader priority implementation. You can use additional semaphores to enforce a certain sequence of events. Note that we will use more sophisticated cases for grading.
In addition, here is a simple test case for the Atom Table:
void AtomSynchTest(int arg)
{
    int value;

    atomTbl->GetAtom("TestAtom", &value);
    printf("^1 TestAtom: %x\n", value);
    currentThread->Yield();

    printf("^Getting the atom again\n");
    atomTbl->GetAtom("TestAtom", &value);
    printf("^2 TestAtom: %x\n", value);

    done.V();
}

void AtomTblTest(int arg)
{
    atomTbl->InsertAtom("TestAtom", 0xBEEF);
    Thread *t3 = new Thread("C1");
    t3->Fork(AtomSynchTest, 0);
    // yield and let the SynchThread wakeup
    currentThread->Yield();
    atomTbl->BlockAtom("TestAtom");
        // the atom is blocked but I should be able to read it myself
    atomTbl->GetAtom("TestAtom", &sum);
    printf("TestAtom Blocked and we are yielding...\n");
    currentThread->Yield();
    atomTbl->UnblockAtom("TestAtom");
    printf("TestAtom UnBlocked and we are waiting...\n");
    done.P();
    printf("Atom Synch test is done\n");

    printf("ATOM TESTS ARE FINISHED\n");
}
void ThreadTest1(int arg)
{
    Thread *t2 = new Thread("AT Test");

    srand(time(0));
    t2->Fork(AtomTblTest, 0);
}

Here is the expected output of this test:
^1 TestAtom: beef
TestAtom Blocked and we are yielding...
^Getting the atom again
TestAtom UnBlocked and we are waiting...
^2 TestAtom: beef
Atom Synch test is done
ATOM TESTS ARE FINISHED

Turning in the project

Part 1 of the lab (RWLock) is due on Tuesday November 2nd, 2004 (election day :-) before 11:59 PM.

Part 2 of the lab (AtomTable) is due on Sunday November 14th, 2004 before 11:59 PM.

Make sure to start early as many of the RWLock and Atom functions are non-trivial.

To submit part 1 of your lab:

  1. Make sure you remove any printf statements you inserted.
  2. Change directories to your threads directory.
  3. Type 'make' and ensure that everything built correctly.
  4. Type 'make clean' to clean up the object files, etc.
  5. Use 'cd ..' to change to the parent directory of your threads subdirectory.
  6. Type 'turnin -c cs354 -p lab5-1 threads' to submit your work for lab 5.
  7. Type 'turnin -c cs354 -p lab5-1 -v' and verify that the files you submitted are correct. Do not forget the -v. If you forget -v, your original submission will be corrupted.
To submit part 2, use the same instructions given above, but replace lab5-1 with lab5-2.