Section notes for CS162 Section #4, February 18, 2003 Hakim Weatherspoon ADMINISTRIVIA - Office hours changed to M 12-1, Tu 3:30-4:30 Location 651 Soda (6th floor alcove) - Project meetings: - Will be done in week after you turn in reports - All members of group must be at meeting - You are graded on the report and the meeting QUIZ T/F Every condition variable is associated with some lock? True. Multiple condition variables may not be associated with the same lock? False. Condition.sleep, wake, wakeAll can be used without holding associated lock? False. Can a thread wait for access to one object while having access to another? True. What does condition.sleep() do? - atomically release the lock and relinkquish the CPU until woken; then reacquire the lock. What does condition.wake() do? condition.wakeAll()? - wake() wakes up a single thread sleeping in 'this' condition variable, if possible. - wakeAll() wakes up all threads sleeping in 'this' condition variable. Whats the difference between Mesa-style condition variables and Hoare style condition variables? What does nachos use? What is the advantage/disadvantage? - Mesa-style condition variables define wake() to wake up another thread and place woken thread on the ready queue. It is the responsibility of the woken thread to reacquire the lock (this reacquire is taken care of in sleep()). By contrast, Hoare-style condition variables define wake() to wake up another thread, give it the lock, and place directly on the CPU (i.e. woken thread runs immediately. Nachos uses Mesa-style semantics. The consequence of using Mesa-style semantics is that some other thread can acquire the lock and change data structures, before the woken thread gets a chance to run. The advance to Mesa-style semantics is that it is a lot easier to implement. REVIEW from last week - Locks, condition variables, semaphores, interrupts, atomic ops - Locks: implement mutual exclusion - Condition variables: make it possible to go to sleep inside a critical section by atomically releasing a lock at the same time the thread goes to sleep - Semaphores: a generalized lock, having a non-negative integer value (classical definition) - Interrupts: an external event which causes the dispatcher to take the CPU away - These things can be built out of each other - Use semaphores to implement condition variables - Use interrupts to implement condition variables (uniprocessor) - Use semaphores to implement a lock - Use interrupts to implement a lock (uniprocessor) - Use TestAndSet to implement a lock PRIORITY INVERSION AND DONATION (using http://inst.eecs.berkeley.edu/~cs162-tc/pri) Priority Scheduling Priority scheduling is used to schedule which process or thread next uses the CPU. Priorities (usually integers) are assigned to each thread and among threads that are ready to run, the one with the highest priority is chosen next. The idea behind priority scheduling is that high priority threads are doing the work we want done first, while threads with lower priority can wait. Usually, threads are kept in a queue, sorted by their priorities so that the highest priority thread is "next" on the queue. Priority Inversion Suppose we have a shared resource. In order to prevent race conditions and inconsistencies, it makes sense to use a lock (mutex). Locks make sure that only one thread is accessing the resource at a time. If a thread wants to access a resource it must acquire the lock first. If it is unable to acquire the lock (in other words, another thread is using the resource), it must wait until the thread currently accessing the resource releases the lock. An example of priority inversion. Thread A, with priority 1, has a lock. Thread C, with priority 3, is waiting on thread A, with priority 1. Thread B, with priority 2, will run before thread C. Threads A, B, and C have priority 1, 2, and 3, respectively. Thread A is holding a lock on a resource that Thread C is wants to use. Thread C must wait for thread A to release the lock because it called acquire on the lock. Because thread C is waiting for the lock, it is not available to run. Thread A and B are ready to run. So when the priority scheduler chooses a thread to run next, it can only choose between A and B. B has a higher priority, so it will go next. Thread A cannot run, so it won't be able to release the lock and thread C will have to wait until B finishes. In other words, the highest priority thread, thread C, is inadvertently being blocked from running by a lower priority thread, thread B. Priority Donation One solution to this problem is to donate priority. In the case we just looked at, if thread A had the same priority as thread C, it would not be blocked. So thread C "donates" its priority to thread A - thread A's priority effectively becomes 3 Continues example of priority donation. Thread C, with priority 3, donates its priority to thread A, with real priority 1. Thread A may now run. When thread A releases the lock, its priority drops down again to its original priority. Thread C is now able to run and the priorities behave as we intended. After priority donation. Thread C, with priority 3, is now able to run and thread A's priority dropped back down to 1. EXAMPLES with producer consumer -------------------------------------------------------------------------- Producers and Consumers using Interrupts -------------------------------------------------------------------------- int numCokes = 0; LinkedList waitingConsumers; LinkedList waitingProducers; Producer() { interrupt.disable(); while (numCokes == MAXCOKES) { waitingProducers.add(myThread); myThread.sleep(); // interrupts still disabled } numCokes++; while (consumer = waitingConsumers.next()) { // wakeAll consumer.ready(); } interrupt.enable(); } Consumer() { interrupt.disable(); while (numCokes == 0) { waitingConsumers.add(myThread); myThread.sleep(); } numCokes--; while (producer = waitingProducers.next()) { // wakeAll producer.ready(); } interrupt.enable(); } -------------------------------------------------------------------------- Producers and Consumers using Condition Variables -------------------------------------------------------------------------- Lock lock; Condition wantToAdd = new Condition(lock); Condition wantToTake = new Condition(lock); Producer() { lock.acquire(); while (numCokes == MAXCOKES) { // Like a semaphore.P() wantToAdd.sleep(); // Lock released! } numCokes++; // Lock reacquired! wantToTake.wakeAll(); // Like a semaphore.V() lock.release(); } Consumer() { lock.acquire(); while (numCokes == 0) { // Like a semaphore.P() wantToTake.sleep(); // Lock released! } numCokes--; // Lock reacquired wantToAdd.wakeAll(); // Like a semaphore.V() lock.release(); } - This is much nicer - Question: What if we changed 'wakeAll()' to 'wake()' in the above? - Answer: Would have to wait for another producer or consumer to come around before the next producer or consumer would wake up! - Note that we're really building "semaphores" when we do it this way! - So different synchronization primitives are more useful or convenient in different situations. ------------------------------------------------------------------------ NACHOS WALKTHROUGH - Homework #1 posted: Need to understand this to do project - Overall: For this project, really need to understand nachos.threads.KThread nachos.threads.ThreadedKernel nachos.machine.TCB (not the internals, just the interface) - Only modify nachos.threads package - nothing else You cannot use Java threads or 'synchronized' keyword - Main structure: nachos.machine.* -- the internals of the implementation Not really important to understand how this works, but need to know what the public methods are Machine.interrupt().disable() Disable interrupts, return flag of previous interrupt state Machine.interrupt().enable() Enable interrupts Machine.interrupt().restore() Restore to previously saved flag TCB.contextSwitch() Context switch to this TCB. Used internally by KThread, you don't need to call this yourself but important to see where it's used TCB.start() Used to bootstrap a new TCB - used internally by KThread nachos.threads.* -- what you will be modifying KThread(Runnable target) Create a new kernel thread and associate with it the code in 'target.run()'. KThread.setName(String name) Associate a new, can be retrieved with getName KThread.fork() Fork the given thread - that is, start it running KThread.yield() Cause the current thread to yield the CPU KThread.sleep() Cause the current thread to block - will be woken up later KThread.ready() Move this thread to the ready queue, i.e. wake it up Lock.acquire() Sleep until this lock can be acquired private KThread lockHolder; intStatus = Machine.interrupt().disable(); if (lockHolder == null) { waitQueue.acquire(KThread.currentThread()); lockHolder = KThread.currentThread(); } else { waitQueue.waitForAccess(KThread.currentThread()); KThread.sleep(); } Machine.interrupt.restore(intStatus); Lock.release() Release the lock (Ask class how this would be implemented) int status = Machine.interrupt().disable(); lockHolder = waitQueue.nextThread(); lockHolder.ready(); // Wake it up Machine.interrupt().restore(intStatus);