Lecture 5 2/2/2005 Topics: ------ SYNCHRONIZATION * Too-Much-Milk Problem continued... * Semaphores > producers and consumers, readers and writers, dining philosophers. * Threads ***************** * Announcements * ***************** - Nachos tutorial tomorrow 2/3 7pm, 306 Soda - If you havn't printed a copy of the class notes, please do so, since they contain administrative information - Will pass out more copies of the questionaire past out during the first lecture - Please sign-up to take lecture notes - Up to date lecture notes have been posted - Bookstore will ship back remaining textbooks at the end of this week *********** * Lecture * *********** --------------- SYNCHRONIZATION --------------- ** The Too-Much-Milk Problem continued... ** Recall the problem: Person A Person B 3:00 Look in fridge. Out of milk. 3:05 Leave for store. 3:10 Arrive at store. Look in fridge. Out of milk. 3:15 Leave store. Leave for store. 3:20 Arrive home, put milk away. Arrive at store. 3:25 Leave store. 3:30 Arrive home. Too much milk. And 4th solution: Process A 1 Leave NoteA; 2 while (NoteB) DoNothing; 3 if (NoMilk) 5 Buy Milk; 7 Remove NoteA; Process B 2 Leave NoteB; 3 if (NoNoteA) 4 if (NoMilk) 6 Buy Milk; 8 Remove NoteB; Even though the 4th solution works it is flawed and is much too complicated. A might have to wait while B is at the store (busy-waiting) and it applies only to 2 roomates. In addition the mutual exclusion mechanism is sub-optimal, since it consists only of atomic reads and writes and would be hard to many processes. ** Semaphores ** Lets look at some more powerfull synchronization mechanisms. Requirement for a mutual exclusion mechanism: a. Must allow only one process into a critical section at a time, if that is what is desired. (mutual exclusion) - In the Too-Much-Milk problem shopping was the critical section. b. If several requests at once, must allow one process to proceed. (progress) c. Processes must be allowed to fail outside critical section. - We eliminated this restriction in the Too-Much-Milk problem by not allowing a roommate to leave town. In addition there are a few things that are desirable to ensure an efficient mutual exclusion mechanism. You don't constant loop testing as we saw in the 4th solution to the Too-Much-Milk problem since this could have a process waiting indefinitely (bounded waiting) and would use up substantial amounts of resources (busy-waiting). We also want it to be simple to use, ex.: aquire mutual exclusion lock release mutual exclusion lock Too ensure that mutual exclusion works correctly it is absolutely crucial that there is NO FAILURE IN THE CRITICAL SECTION! Outside it ok. Q How do we define what we want to lock? A Whatever we need to make the algorithm work right is what we lock. Now on to semaphores, yeeha! Definitions: a. Running - process is actively receiving CPU cycles. b. Blocked - process is not able to run, because it is waiting for something - e.g. I/O, synchronization, etc. c. Semphore - A synchronization variable that takes on non-negative integer values. There are 2 operations that can be performed on a semaphores: Note (Smith): Students should appriciate that the US has lead the CS field the last couple years. As you will notice the following definitions are not in english. P(semaphore) - P = "proberen", meaning "test". an atomic operation that attempts to decrement a semaphore. If the semaphore is positive, it succeeds. If the semaphore is 0, the process is blocked until some other process increments it. V(semaphore) - V = "verhogen", meaning "increment". an atomic operation on a semaphore. If there is at least one process blocked on this semaphore, one is released. Otherwise, the semaphore is incremented. Q How is the semaphore useful? A Semaphores are free and can be initialized to whatever is appropriate. A trivial example will be shown. Semaphores are usually not implemented in the hardware of most machines beacuse they are too complicated. Instead they are implemented in software using simpler synchronization variables provided in the hardware. Milk much milk problem with semaphores: Processes A & B 1 P(OKToBuyMilk); 2 if (NoMilk) BuyMilk; 3 V(OKToBuyMilk); OKToBuyMilk must initially be set to 1, since otherwise A & B will both block on line 1 and never enter the critical section. Once a process decrements OKToBuyMilk to 0 the other process will block until the other process has completed the critical section and released the lock. Thus no two processes can enter the critical section at the same time. The type of semaphore used in the solution to the Too-Much-Milk problem above is called a "Binary Semaphore" since it can only take on the values 0 and 1. Counting semaphores on the other hand are semaphores whose value is used as a count of some sort. Q Does the # of the semaphore mean how many processes can use the resource at the same time? A No Semaphores have several useful properties: a. Not machine dependent b. Simple, which means you can solve harder problems c. Provide mutual exclusion d. Provide waiting (not busy-waiting) e. Correctness relatively simple to determine f. Usually work for N processes, not just 2. g. Can get multiple resources by using multiple semaphores in sequence I.e. If we need multiple resources,issue a P for one, then another P for the next. Semaphores are used in 2 different ways: 1. Mutual exclusion: provided by binary sempahores 2. Scheduling: binary or counting semaphores Next we will be discussing 3 well known and important problems that all go back atleast 40 years. >> Producers & Consumers << An analogy for this problem would be a copper mine containing hopper cars. The producers fill hopper cars with copper and add them to a chain of cars which are then emptied by consumers at the exit. After a car has been emptied it returns to the mine where it can be refilled. We don't want producers and consumers accessing the same hopper car at the same time and producers should be able to get ahead of the consumers. hopper with copper |x| |x| |x| |x| /----------------------\ inside mine | | out side mine (producers) \----------------------/ (consumers) | | | | | | | | empty hopper cars Definitions: Producer - creates copies of a resource (fills hopper car). Consumer - uses up (destroys) copies of a resource (empties hopper car). Buffer - used to hold resource after producer has created it but before consumer has used it (hopper car). Synchronization - keeping producer and consumer in step. Constraints: a. Consumer must wait for producer to fill buffers. (scheduling) b. Producer must wait for consumer to empty buffers, if all buffer space is in use. (scheduling) c. Only one process must fiddle with buffer pool at once. (mutual exclusion) A separate semaphore is used for each constraint. Initialization: Put all buffers in pool of empties. Initialize semaphores: empties = N, fulls= 0, mutex = 1; Producer process: 1 P(empties); 2 P(mutex); 3 get empty buffer from pool of empties; 4 V(mutex); 5 produce data in buffer; 6 P(mutex); 7 add full buffer to pool of fulls; 8 V(mutex); 9 V(fulls); Consumer process: 1 P(fulls); 2 P(mutex); 3 get full buffer from pool of fulls; 4 V(mutex); 5 consume data in buffer; 6 P(mutex); 7 add empty buffer to pool of empties; 8 V(mutex); 9 V(empties); Important questions: - Why does producer P(empties) but V(fulls)? P(empties) is called to acquire an empty buffer. If there are no more empty buffers the producer will block until there is one. V(fulls) adds a full buffer that can now be emptied by a consumer. - Why is order of P's important? P(empties) and P(mutex) can't be switched since this might create a deadlock. Ex. Producer calls P(mutex) and now tries to get an empty cart. However there are no empty hopper carts and the consumer can't empty one since the producer has the lock. The same logic is true for P(fulls) and P(mutex). - Is order of V's important? No, ordering them either way will work. - Could we have separate semaphores for each pool? Yes, in fact, it would be more efficient, if the pools were separate. Would have mutex1 for empties and mutex2 fulls. - How would this be extended to have 2 (or N) consumers? Nothing, will work for any number of consumers. THIS IS A VERY IMPORTANT EXAMPLE! a. Because its simple b. Because it occurs alot >> Readers and writers problem << See notes for lecture 4&5 where this problem is fully explained. (~cs162/Students/Lecture_Notes_05.s/smith.2.txt) Below are answers to the questions posed. Questions: - In case of conflict between readers and writers, who gets priority? Writers. - Is the WW necessary in the writer's first if? No, since if AW and AR are 0 WW is 0. - Can OKToRead ever get greater than 1? No. - What about OKToWrite? No. - Is the first writer to execute P(Mutex) guaranteed to be the first writer to access the data? No. >> Dining Philosophers Problem << See notes for lecture 4&5 where this problem is fully explained. (~cs162/Students/Lecture_Notes_05.s/smith.2.txt) ** Threads ** Threads are a type of proccess, known as a lightweight process. Note (Smith): It is recommended they you read paper #5 in the reader, however you will not be tested on it. Like a proccess a thread has its own program counter, register set values, and stack. It shares with 1 or more threads its code, data, and OS resources such as open files. All threads that are sharing a common state are known collectively as a "task". A task with only 1 thread is an ordinary (heavy weight) process. Creating and switching between threads is much cheaper than creating or switching processes, since threads share the resources of the process to which they belong. In Solaris for example, creating a process is about thirty times slower than creating a thread, and context switching is about five times slower. To see why this might be so, in swithiching between threads you only need to reload user registers and not change the entire Process control block (PCB, see OSC w/ Java page 106). Switching threads can be done by a user, whereas processes can only be switched by the OS. If the user is switching threads the OS does'nt know about it, so if one thread gets blocked they all get blocked. Threads can fork off more threads, which share memory and thus are low in overhead to create, however they protected from each other. I.e., they can overwrite each other! | original process | |\ | \ thread 2 | \ thread 2 | | | \/ \/ All threads will crash if one goes berserk. Why use threads? a. On Uniprocessor, may provide more convenient model for programming than normal sequential program. (Does not inherantly provide higher efficiency.) b. On (shared memory) Multiprocessor, may provide parallelism, since different threads can run on different processors in parallel. c. Note that OS must do scheduling if multiprocessors involved (at least to set them up and running on each processor.) d. Lower overhead (task switching, memory sharing) than separate parallel (heavyweight) processes. Note (Smith): back in the old days when transistor radios came out they were advertised by how many transistors they had. This had no bearing on how good the radio was and is a parallel to how intel has been advertising it processors hyperthreading technology.