Synchronization and Priority Scheduler
Nachos has the ability to use different thread scheduling algorithms as specified in its configuration file (you used the RoundRobin scheduler in Project 1). In this project you will implement a Priority scheduler and solve a synchronization problem.
For this project you will submit two zipped files. The first file should be named project2class.zip and must include all files in your proj1/nachos/threads directory (this directory only contains class files generated during compilation). The second file should be named project2code.zip and must include all files in your threads directory (this is a directory in the root directory of the nachos installation). This directory contains your .java files (source code). Submission of these two files is by email (firstname.lastname@example.org). Both files should be included as attachments in one email message. The subject of your message must be "CS3053-Project 2" and the body of your message should contain the names of the authors.
Additionally, you will have to write a report describing your approach in solving each one of the tasks indicated below, including any test scenarios you may have considered during development (some of them will only take a few lines, some of them will take a few paragraphs). Please include a Feedback section in your report. The report should be well written and well organized.
1. Using synchronization devices provide a solution to a problem involving shared use of a boat to get people from one island to another. Persons are to be represented by individual threads. A person can be either and adult or a child (two types of persons or threads).
2. Implement priority scheduling in Nachos by completing the provided PriorityScheduler class. Your solution must implement priority donation to address priority inversion
For this project you will work using the nachos/proj1 directory. Please remember to modify nachos.conf to switch schedulers. Use the round-robin scheduler to solve the synchronization problem.
Task 1 – Synchronization
Now that you have all the synchronization devices implemented, use them to solve this problem. You will find condition variables to be the most useful synchronization method for this task.
A number of Hawaiian adults and children are trying to get from Oahu to Molokai. Unfortunately, they have only one boat which can carry maximally two children or one adult (but not one child and one adult). The boat can be rowed back to Oahu, but it requires a pilot to do so.
Arrange a solution to transfer everyone from Oahu to Molokai. You may assume that there are at least two children.
The method Boat.begin() should fork off a thread for each child or adult. Your mechanism cannot rely on knowing how many children or adults are present beforehand, although you are free to attempt to determine this among the threads (i.e. you can't pass the values to your threads, but you are free to have each thread increment a shared variable, if you wish).
To show that the trip is properly synchronized, make calls to the appropriate BoatGrader methods every time someone crosses the channel. When a child pilots the boat from Oahu to Molokai, call ChildRowToMolokai. When a child rides as a passenger from Oahu to Molokai, call ChildRideToMolokai. Make sure that when a boat with two people on it crosses, the pilot calls the ...RowTo... method before the passenger calls the ...RideTo... method.
Your solution must have no busy waiting, and it must eventually end. Note that it is not necessary to terminate all the threads -- you can leave them blocked waiting for a condition variable. The threads representing the adults and children cannot have access to the numbers of threads that were created, but you will probably need to use these numbers in begin() in order to determine when all the adults and children are across and you can return.
The idea behind this task is to use independent threads to solve a problem. You are to program the logic that a child or an adult would follow if that person were in this situation. For example, it is reasonable to allow a person to see how many children or adults are on the same island they are on. A person could see whether the boat is at their island. A person can know which island they are on. All of this information may be stored with each individual thread or in shared variables. So a counter that holds the number of children on Oahu would be allowed, so long as only threads that represent people on Oahu could access it.
What is not allowed is a thread which executes a "top-down" strategy for the simulation. For example, you may not create threads for children and adults, then have a controller thread simply send commands to them through communicators. The threads must act as if they were individuals.
Information which is not possible in the real world is also not allowed. For example, a child on Molokai cannot magically see all of the people on Oahu. That child may remember the number of people that he or she has seen leaving, but the child may not view people on Oahu as if it were there. (Assume that the people do not have any technology other than a boat!)
The one exception to these rules is that you may use the number of people in the total simulation to determine when to terminate. This number must only be used for this purpose.
Task 2 – Priority Scheduling
Implement priority scheduling in Nachos by completing the PriorityScheduler class. Priority scheduling is a key building block in real-time systems. Note that in order to use your priority scheduler, you will need to change a line in nachos.conf that specifies the scheduler class to use. The ThreadedKernel.scheduler key is initially equal to nachos.threads.RoundRobinScheduler. You need to change this to nachos.threads.PriorityScheduler when you're ready to run Nachos with priority scheduling.
Priorities in nachos range from 0 to 7 (integer value), with 7 being the maximum priority. Note that all scheduler classes extend the abstract class nachos.threads.Scheduler. You must implement the methods getPriority(), getEffectivePriority(), and setPriority(). You may optionally also implement increasePriority() and decreasePriority() (these are not required). In choosing which thread to dequeue, the scheduler should always choose a thread of the highest effective priority.
An issue with priority scheduling is priority inversion. If a high priority thread needs to wait for a low priority thread (for instance, for a lock held by a low priority thread), and another high priority thread is on the ready list, then the high priority thread will never get the CPU because the low priority thread will not get any CPU time. A fix for this problem is to have the waiting thread donate its priority to the low priority thread while it is holding the lock.
Implement the priority scheduler so that it donates priority, where possible. Be sure to implement Scheduler.getEffectivePriority(), which returns the priority of a thread after taking into account all the donations it is receiving.
It is important that you do not break the abstraction barriers while doing this part -- the Lock class does not need to be modified. Priority donation should be accomplished by creating a subclass of ThreadQueue that will accomplish priority donation when used with the existing Lock class, and still work correctly when used with the existing Semaphore and Condition classes.