Ken Birman Review: Monitors Why review something we just covered? People find it hard to get into the “concurrency mindset” It takes time to get used to this style of coding, and to thinking about the correctness of concurrent objects So: review a little, then discuss a related topic, and then (next time) tackle a new concurrency issue Yes, this isn’t “linear”… … but avoids “too much, too fast” … Huh? Producer Consumer using Monitors public class Producer_Consumer { int N; Object[] buf; int n = 0, tail = 0, head = 0; Object not_empty = new Object(); Object not_full = new Object(); public Producer_Consumer(int len) { buf = new object[len]; N = len; } public Object get() { Object obj; synchronized(not_empty) { while(n == 0) not_empty.wait(); object obj = buf[tail%N]; tail++; synchronized(this) { n--; } } synchronized(not_full) { not_full.notify(); } return obj; } public void put(Object obj) { } synchronized(not_full) { while(n == N) not_full.wait(); buf[head%N] = obj; head++; synchronized(this) { n++; } } synchronized(not_empty) { not_empty.notify(); } } 3 Subtle aspects When updating a variable shared by producer and consumer, the code needs to lock it Hence synchronized(this) { n++; } But we can read the variable without interlocks: while(n == N) not_full.wait(); 4 Subtle aspects To call wait or notify, must first call synchronized on the object that you will wait or notify on, hence synchronized(xyz) { xyz.wait(); } synchronized(xyz) { xyz.notify(); } … if our code had been synchronized on something else, you get a “thread isn’t the owner of this object” exception When you do call wait, do it inside a while loop and recheck the condition when you wake up while(n == N) NotFull.wait(); This way, if someone wakes up the thread (because n < N) but some other thread sneaks in and now n==N again, your code will be safe. 5 Subtle aspects Notice that when a thread calls wait(), if it blocks it also automatically releases the lock on the object on which wait was done This is an elegant solution to an issue seen with semaphores Caller did mutex.acquire()…. But now needs to call not_empty.aquire()… and this second call might block So we need to call mutex.release()… Danger is that some other thread “slips in” between the two A race condition! 6 Subtle aspects But…. Java has a bug… … nested synchronized() calls are “risky” If the inner block calls wait, the outer lock won’t be automatically released Can easily result in deadlocks This is why our bounded buffer code synchronized on Not_Full… … although sometimes we can even take advantage of this annoying behavior 7 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 8 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 9 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 10 Understanding the Solution If any thread is waiting, no other thread gets into BeginRead or BeginWrite This is because of the “bug” mentioned before When we nest synchronization blocks, a wait will only release the lock on the object we wait on… not the outer synchronization lock Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 12 Understanding the Solution A writer can enter if there are no other active writers and no readers are waiting 13 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 14 Understanding the Solution A reader can enter if There are no writers active or waiting So we can have many readers active all at once Otherwise, a reader waits (maybe many do) 15 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 16 Understanding the Solution When a writer finishes, it lets one thread in, if someone is waiting Doesn’t matter if this is one writer, or one reader If a reader gets in, and then another shows up, it will get in too… once it gets past the “entry” synchronization Similarly, when a reader finishes, if it was the last reader, it lets a writer in (if any is there) 17 Readers and Writers public class ReadersNWriters { int NReaders = 0, NWriters = 0; Object CanBegin = new Object(); public synchronized void BeginWrite() { synchronized(CanBegin) { if(NWriters == 1 || NReaders > 0) CanBegin.Wait(); NWriters = 1; } } public void EndWrite() { synchronized(CanBegin) { NWriters = 0; CanBegin.Notify(); } } public synchronized void BeginRead() { synchronized(CanBegin) { if(NWriters == 1) CanBegin.Wait(); ++NReaders; } } public void EndRead() { synchronized(CanBegin) { if(--NReaders == 0) CanBegin.Notify(); } } } 18 Understanding the Solution It wants to be fair If a writer is waiting, readers queue up “outside” If a reader (or another writer) is active or waiting, writers queue up 19 Comparison with Semaphores With semaphores we never had a “fair” solution In fact it can be done, in much the same way The monitor is relatively simple… and it works! In general, monitors are relatively less error prone Still, this particular one isn’t so easy to derive or understand, because it makes subtle use of the synchronization lock Our advice: teams should agree to use monitor style 20 Scheduling Why discuss? Hidden under the surface in our work up to now is the role of the scheduler It watches the pool of threads, or processes And when the current thread/process waits, or has been running for too long If necessary, “preempts” the current thread/process Selects something else to run Context switches to it OK… so who cares? We’ve worried about the fairness of our solutions But implicit in this is the assumption that the scheduler itself is reasonably fair At a minimum, that every runnable thread/process gets plenty of opportunities to run Let’s look more closely at how schedulers really work Process Scheduling Rest of lecture: “process” and “thread” used interchangeably Many processes in “ready” state Which ready process to pick to run on the CPU? 0 ready processes: run idle loop 1 ready process: easy! But if > 1 ready process: what to do? New Ready Running Waiting Exit Some schedulers have no choice For example, in Java, if you “notify” a thread, it will be the next thread to obtain the synchronization lock … even if other threads are waiting But often, there are situations with Multiple active processes, or threads All want to run… When does scheduler run? Non-preemptive minimum Process runs until voluntarily relinquish CPU process blocks on an event (e.g., I/O or synchronization) process terminates Preemptive minimum All of the above, plus: New Event completes: process moves from blocked to ready Timer interrupts Implementation: process can be interrupted in favor of another Running Ready Waiting Exit Process Model Process alternates between CPU and I/O bursts CPU-bound jobs: Long CPU bursts Matrix multiply I/O-bound: Short CPU bursts Editor emacs I/O burst = process idle, switch to another “for free” Problem: don’t know job’s type before running An underlying assumption: “response time” most important for interactive jobs (I/O bound) Scheduling Evaluation Metrics Many quantitative criteria for evaluating sched algo: CPU utilization: percentage of time the CPU is not idle Throughput: completed processes per time unit Turnaround time: submission to completion Waiting time: time spent on the ready queue Response time: response latency Predictability: variance in any of these measures The right metric depends on the context “The perfect CPU scheduler” Minimize latency: response or job completion time Maximize throughput: Maximize jobs / time. Maximize utilization: keep I/O devices busy. Recurring theme with OS scheduling Fairness: everyone makes progress, no one starves Problem Cases Blindness about job types I/O goes idle Optimization involves favoring jobs of type “A” over “B”. Lots of A’s? B’s starve Interactive process trapped behind others. Response time sucks for no reason Priorities: A depends on B. A’s priority > B’s. B never runs Scheduling Algorithms FCFS First-come First-served (FCFS) (FIFO) Jobs are scheduled in order of arrival Non-preemptive Problem: Average waiting time depends on arrival order P1 P2 0 16 P2 0 P3 4 P3 time 20 24 P1 8 Advantage: really simple! 24 Convoy Effect A CPU bound job will hold CPU until done, or it causes an I/O burst rare occurrence, since the thread is CPU-bound long periods where no I/O requests issued, and CPU held Result: poor I/O device utilization Example: one CPU bound job, many I/O bound CPU bound runs (I/O devices idle) CPU bound blocks I/O bound job(s) run, quickly block on I/O CPU bound runs again I/O completes CPU bound still runs while I/O devices idle (continues…) Simple hack: run process whose I/O completed? What is a potential problem? Scheduling Algorithms LIFO Last-In First-out (LIFO) Newly arrived jobs are placed at head of ready queue Improves response time for newly created threads Problem: May lead to starvation – early processes may never get CPU Problem You work as a short-order cook Customers come in and specify which dish they want Each dish takes a different amount of time to prepare Your goal: minimize average time the customers wait for their food What strategy would you use ? Note: most restaurants use FCFS. Scheduling Algorithms: SJF Shortest Job First (SJF) Choose the job with the shortest next CPU burst Provably optimal for minimizing average waiting time P1 P3 0 15 PP2 2 PP3 3 0 3 Problem: P2 21 24 P1 9 24 Impossible to know the length of the next CPU burst Scheduling Algorithms SRTF SJF can be either preemptive or non-preemptive New, short job arrives; current process has long time to execute Preemptive SJF is called shortest remaining time first P2 P3 0 P1 6 P3 0 P1 6 21 10 P2 10 13 P1 24 Shortest Job First Prediction Approximate next CPU-burst duration from the durations of the previous bursts The past can be a good predictor of the future No need to remember entire past history Use exponential average: tn duration of the nth CPU burst n+1 predicted duration of the (n+1)st CPU burst n+1 = tn + (1- ) n where 0 1 determines the weight placed on past behavior Priority Scheduling Priority Scheduling Choose next job based on priority For SJF, priority = expected CPU burst Can be either preemptive or non-preemptive Problem: Starvation: jobs can wait indefinitely Solution to starvation Age processes: increase priority as a function of waiting time Round Robin Round Robin (RR) Often used for timesharing Ready queue is treated as a circular queue (FIFO) Each process is given a time slice called a quantum It is run for the quantum or until it blocks RR allocates the CPU uniformly (fairly) across participants. If average queue length is n, each participant gets 1/n RR with Time Quantum = 20 Process P1 P2 P3 P4 The Gantt chart is: P1 0 P2 20 P3 37 P4 57 Burst Time 53 17 68 24 P1 77 P3 P4 97 117 P1 P3 P3 121 134 154 162 Higher average turnaround than SJF, But better response time Turnaround Time w/ Time Quanta RR: Choice of Time Quantum Performance depends on length of the timeslice Context switching isn’t a free operation. If timeslice time is set too high attempting to amortize context switch cost, you get FCFS. i.e. processes will finish or block before their slice is up anyway If it’s set too low you’re spending all of your time context switching between threads. Timeslice frequently set to ~100 milliseconds Context switches typically cost < 1 millisecond Moral: Context switch is usually negligible (< 1% per timeslice) unless you context switch too frequently and lose all productivity Scheduling Algorithms Multi-level Queue Scheduling Implement multiple ready queues based on job “type” interactive processes CPU-bound processes batch jobs system processes student programs Different queues may be scheduled using different algos Intra-queue CPU allocation is either strict or proportional Problem: Classifying jobs into queues is difficult A process may have CPU-bound phases as well as interactive ones Multilevel Queue Scheduling Highest priority System Processes Interactive Processes Batch Processes Student Processes Lowest priority Scheduling Algorithms Multi-level Feedback Queues Implement multiple ready queues Different queues may be scheduled using different algorithms Just like multilevel queue scheduling, but assignments are not static Jobs move from queue to queue based on feedback Feedback = The behavior of the job, e.g. does it require the full quantum for computation, or does it perform frequent I/O ? Very general algorithm Need to select parameters for: Number of queues Scheduling algorithm within each queue When to upgrade and downgrade a job Multilevel Feedback Queues Highest priority Quantum = 2 Quantum = 4 Quantum = 8 FCFS Lowest priority A Multi-level System high I/O bound jobs priority CPU bound jobs low timeslice high Thread Scheduling Since all threads share code & data segments Option 1: Ignore this fact Option 2: Gang scheduling run all threads belonging to a process together (multiprocessor only) if a thread needs to synchronize with another thread the other one is available and active Option 3: Two-level scheduling: Medium level scheduler schedule processes, and within each process, schedule threads reduce context switching overhead and improve cache hit ratio Option 4: Space-based affinity: assign threads to processors (multiprocessor only) improve cache hit ratio, but can bite under low-load condition Real-time Scheduling Real-time processes have timing constraints Expressed as deadlines or rate requirements Common RT scheduling policies Rate monotonic Just one scalar priority related to the periodicity of the job Priority = 1/rate Static Earliest deadline first (EDF) Dynamic but more complex Priority = deadline Both require admission control to provide guarantees Postscript The best schemes are adaptive. To do absolutely best we’d have to predict the future. Most current algorithms give highest priority to those that need the least! Scheduling become increasingly ad hoc over the years. 1960s papers very math heavy, now mostly “tweak and see ” Problem What are metrics that schedulers should optimize for ? There are many, the right choice depends on the context Suppose: You own an airline, have one expensive plane, and passengers waiting all around the globe You own a sweatshop, and need to evaluate workers You are at a restaurant, and are waiting for food You are an efficiency expert, and are evaluating government procedures at the DMV You are trying to find a project partner or a co-author You own a software company that would like to deliver a product by a deadline