CS 162 Lecture Notes 1/31/07 Robert Taylor Copy Central (Northside) should now have the Nachos reader Topic: Examples of Semaphore Usage --Producer / Consumer example Problem: Have two processes, a producer and a consumer process. The producer creates data that will be read by the consumer. Want to make sure consumer is not waiting for producer to make data or producer is waiting for free memory to write to. Solution: The issue is to synchronize two so when one is writing data other is not reading the data. We solve it by using buffers so the two process are in lock step-- one can't operate while the other is on the same piece of data. Implementation: The "producer" thread has a linked list of empty buffers; the producer fills up its empty buffers one at a time and transfers them over to the linked list of the "consumer" so the consumer thread may read the data in the buffer, then clear the buffer and send it back to the linked list of the "producer". The "critical section" of code, which contains shared data usage and must only be accessed by one thread at a time, is locked when the mutex (mutual exclusion) variable is set to 1. Two semaphores are used: the mutex variable for protecting the critical code, and two counts, one each for the producer and consumer, keeping track of how many buffers are in their list. Code with comments: //Producer process: P(empties); //if there are no empty buffers, P can't actually write //anything, so block it until free buffers come in. Basically make sure we have an //empty buffer. P(mutex); //empty buffer now exist, time to do something, so lock the //critical section of code with mutex. Don't let any new empty buffers come //in to pool. get empty buffer from pool of empties; //<--- V(mutex); //unlock the critical section now that empty buffer obtained produce data in buffer; //<--- P(mutex); //lock critical section again while putting in consumer's list add full buffer to pool of fulls; //<--- V(mutex); //done making changes, unlock V(fulls); //unlock consumer so it may use buffer producer just made //For last line above, professor adds "increment number of full buffers" //Consumer process: P(fulls); //if there aren't any full buffers, lock up until something arrives //^(nothing to read otherwise) P(mutex); //lock critical section as before get full buffer from pool of fulls; //<--- V(mutex); //unlock now that buffer received consume data in buffer; //<--- P(mutex); //lock again now that we need to move buffers around add empty buffer to pool of empties; //<--- V(mutex); //unlock now that done with transfer V(empties); //allow producer to start writing to buffer(s) now that we //read and cleared them. For last line above, professor adds "note mutex //to lock access on each change in buffer" Improving this code: Current problem is we only have one mutex variable: when we lock it, both producer and consumer blocked. Really we just want to lock either the consumer or producer with it depending on whose linked list is being added to. Would be better to have mutex1 mutex2 to only lock one data struct at a time Questions (in lec notes): 1. Why does producer P(empties) but V(fulls)? See comments on code above. 2. Why is order of P's important? Rephrased, does P(empties) need to come before P(mutex) at top? Yes. Say it doesnt come first, and we block on empties-- then consumer cant bump up empties later. If you dont make sure there's an empty buffer before locking critical section, then with P(empty) you wait for an empty buffer while locked, and you won't get it because consumer cant get into crit section to modify pool of empties because you locked it with P(mutex). ^^This is an example of deadlock. 3. Is order of V's important. No. Doesn't matter if V(mutex) comes before V(empties). Basically V is not blocking, it unblocks, so order not important. 4. Could we have separate semaphores for each pool? Yes, see "Improving the Code" above. 5. How would this be extended to have 2 (or N) consumers? You would probably want 2 or N pools, and another semaphore for each pool. --Readers and Writers Problem Goal: want consisntent results from shared database. Can have many users reading, but only one writer writing (at a time). Note a write includes the whole set of read-modify write, not just write. Use semaphores to implement--when modifying a shared state, use a semaphore to block access to it. Implementation notes: A writer is like a bookmark, everything completes before him before he goes, everything after him only goes after he's done. State variables used: AR = number of active readers (initially set to 0). WR = number of waiting readers (initially set to 0) AW = number of active writers (initially set to 0). WW = number of waiting writers (initially set to 0). Note by our self imposed rule, AW will always be 0 or 1. Additionally, AR and AW may not both be non-zero as we cannot be reading and writing at same time. Semaphores used: OKToRead (initially set to 0) OKToWrite (initially set to 0) Mutex (initially set to 1) Code with comments: //Reader process P(Mutex); //get a lock because might try to read shared data if ((AW+WW) == 0) { //if no one is writing or waiting to write (since write has //priority over read), V(OKToRead); //unlock the read semaphore. AR = AR+1; //incr num of active writers } else WR = WR+1; //We can't read right now because writing or waiting to write. //Incr waiting readers then. V(Mutex); //Unlock - not modifying AR/AW/WR/WW for now P(OKToRead); //lock so we can start reading read the necessary data; P(Mutex); //Going to modify AR/AW/WR/WW so lock again AR = AR-1; //now that someone who wanted to read did, decr number of active readers if (AR==0 && WW>0) {//if we're done reading and someone waiting to write, V(OKToWrite); //since we know no readers currently reading, start writing AW = AW+1; //incr AW (from 0 to 1) WW = WW-1; //no longer waiting to write } V(Mutex); //done modifying variables for now, unlock semaphore //Writer process P(Mutex); if ((AW+AR+WW) == 0) { //if no active reads or writes then OK to write. DONT need WW -- //if nothing is active in system then nothing should be waiting, it would have been made active. V(OKToWrite); //Someone wanted to write, and system not busy, so let's write AW = AW+1; //AW from 0 to 1 } else WW = WW+1; //system busy, not ok to write, keep OKToWrite locked, and //add to waiting write. V(Mutex); //done writing variables, unlock P(OKToWrite); //re-lock oktowrite... basically wait until its ok to actually write //(will happen in reader section above with V(OKToWrite) I think) P(Mutex); // going to modify variables again, lock variables again AW = AW-1; //not sure why this is here.. if (WW>0) {//if there's a waiting write (likely in case we couldn't make active write in above code), V(OKToWrite); //unlock OKToWrite semaphore so we can do writing AW = AW+1; //waiting write is now active so incr AW and... WW = WW-1; //decr WW } else while (WR>0) { //if no pending writes, but pending reads, then get reads out of the way V(OKToRead); //unlock semaphore AR = AR+1; //took a waiting read and made it an active read, so incr AR and.. WR = WR-1; //decr WR } V(Mutex); //done writing variables unlock semaphore Note how we are always giving permission before a request occurs. For clarification, the V operation checks to see if any operation blocked in semaphore //and release it, while P locks process in semaphore. It's as though you give permission //in one place and lose permission elsewhere to do something else. Questions on above: 1. If there's a conflict between readers and writers who gets priority? The writers. If readers go before writers, the writers may never get to go. Also note writers go once readers before them go, then all writes happen, then all reads after happen (writes are like "bookmarks". 2. IS the WW necessary in the writer's first if? No, see comment on that line. 3. Can OKToRead ever get greater than 1? What about OKToWrite? OKToRead: it depends on how fast we queue and dequeue things. Its possible that the semaphore goes above 1 if we can't release things as we receive them. Ie order of increment release is not necessarily incr-release-incr-relase but may be inc-inc-rel-rel, allowing OKTR to go above 1. Note this is really possible at all because we allow more than one process to read at once if the situation arises. OKToWrite: There's no more than one process ever writing so never greater than one. 4.Is the first writer to execute P(Mutex) guaranteed to be the firsnt writer to access data? No. The point of mutex is to guarantee only one process is in critical section, not to guarantee the right order for this kind of thing. Which writer gets to proceed by mutex depends on semaphore, which is not necessarily the order in which they arrived. More resource sharing: the "Dining Philosophers" problem Philosophers sitting at a round table with one fork between each philosopher. Each P needs to forks to eat though, so how do we synchronize fork use so everyone can eat and no one "starves"? We want to be efficient so more than 1 can eat at the same time. Technically with 5 philosophers 2 should be able to eat at once. Obvious solution of everyone picking up the fork to their left then the fork to their right causes lots of deadlock. Second solution: 1. pick up left fork, 2. if available, pick up right fork, else, a. put down left fork, b. wait for right fork, c. pick up right fork, d. pick up left fork; wait if necessary, 3. eat Bad because if everyone synchronized then everyone picks up left fork, no right fork available, switch, go back and forth synchronized forever, etc. Basically same problem as obvious solution except everyone waiting for right fork instead of left. "Real" solution (one that works): For N+1 (NOT N FROM ORIGINAL LECTURE NOTES BELOW) philosophers, have a mutex semaphore initialized to 1; an array from 0 to N each element H[i] representing each philosopher i's state: hungry, !hungry, eating an array from 0 to N each element prisem[i] representing a private semaphore for each philosopher Code and Comments: loop: P(mutex) //modifying H array, set mutex semaphore H(me):=hungry; //set my state to hungry test(me) //see if I can eat (see below) V(mutex) //done modifying H array P(prisem(me)) //wait till I can eat. if this makes no sense at all, which it //shouldnt at this point in the code, see below eat //delicious P(mutex) //modifying H again set semaphore H(me):=not hungry //just ate, so set my state to not hungry test(left) //to prevent everyone from being forever locked with P(prisem(me)) above, //you dutifully check your neighbors to see if they can be unblocked //(they cant test/unblock themselves when blocked). This also allows you to immediately //free forks on either side of you for others to potentially use right away. test(right) //same V(mutex) //done modifying H array procedure test(me): //Basically if people on either side not eating (both forks free) then say I'm eating //and unlock my private semaphore if H((left)!=eating and H(me)=hungry and H(right of me)!= eating) H(me):=eating V(prisem(me)) ======================================================== Topic: Threads vs processes --Definitions: --A process is a stream of execution and the corresponding state. --Thread: multiple streams of execution sharing a state. --State: may have characteristics such as open files, etc --Task: is the set of threads sharing code data etc. A task with one thread is an ordinary "heavyweight" process; multiple threads within a task are referred to as lightweight processes). Note multiple threads are not called a process but rather a task. --Difference in a nutshell: Process == 1 stream of execution, thread is >1 stream of execution sharing same Why have threads? --Convenient especially if what you're programming should be inherently paralell --Switching overhead for threads costs less than switching overhead for processes, because maintain PCB and other variables in thread switching but not in process switching. --However, obvious and immediate problem in that shared memory between threads is not protected between threads On a uniprocessor, may be convenient but not necessarily more efficient. Best on multiprocessor with paralellism--run multiple threads at once. --- Hardware uses of semaphores: Can in one instruction read, modify and write to a semaphore. Might use a variable A initialzed to 1; first process to come in checks if A=1, which is the unlocked state. Seeing it is 1, the process increments A to 2, runs the critical code, then decrements A back to 1. If a process came in while A was at 2, it would wait in a while loop until it saw A was 1, at which point it would go. Can be broken with a 5 process example: first process comes in, A goes from 0 to 1.2nd process comes in, A goes to 2 and locked. Keep incrementing to 5. The first guy finishes, and decremnts A back to 4. The problem is, the other four processes keep checking if A is 1, but A is stuck now at 4 forever. Basic code: A is initialized to 0; when a process comes arrives: A=A+1; //increment A, for below when we see if now A=1, the unlocked state while A!=1 { /*check forever to see if A = 1. Note if any process arrived before the first process ever to arrive finished, there would be a problem...*/ } run critical code; A=A-1; ================================================== END OF STUDENT NOTES; BEGINNING OF PROFESSOR NOTES ================================================== + Semaphore Example: + Producers & Consumers:. + Suppose one process is creating information that is going to be used by another process, e.g. suppose one process is read- ing information from the disk, and another process will com- pile that information from soure code to binary. Processes shouldn't have to operate in perfect lock-step: producer should be able to get ahead of consumer. + Producer: creates copies of a resource. + Consumer: uses up (destroys) copies of a resource. (may produce something else) + Buffers: used to hold information after producer has created it but before consumer has used it. + Synchronization: keeping producer and consumer in step. Picture of 2 processes with empty and full lists. Do with lists, not circular buffer. + Define constraints (definition of what is ``correct''). Note importance of doing this before coding + Consumer must wait for producer to fill buffers. (scheduling) + Producer must wait for consumer to empty buffers, if all buffer space is in use. (scheduling) + 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: P(empties); P(mutex); get empty buffer from pool of empties; V(mutex); produce data in buffer; P(mutex); add full buffer to pool of fulls; V(mutex); V(fulls); + Consumer process: P(fulls); P(mutex); get full buffer from pool of fulls; V(mutex); consume data in buffer; P(mutex); add empty buffer to pool of empties; V(mutex); V(empties); + Important questions: + Why does producer P(empties) but V(fulls)? + Why is order of P's important? + Is order of V's important? (NO) + Could we have separate semaphores for each pool? (Yes, in fact, it would be more efficient, if the pools were separate. Would have mutex1 and mutex2.) + How would this be extended to have 2 (or N) consu- mers? + Producers and consumers produces something much like Unix pipes. + THIS IS A VERY IMPORTANT EXAMPLE! + Readers and Writers Problem + A shared database with readers and writers. + It is safe for any number of readers to access the data- base simultaneously, but each writer must have exclusive access. + Note that a "write" is usually a "read-modify-write". + Must use semaphores to enforce these policies. + Example: checking account (statement-generators are readers, tellers are writers). + Writers are actually readers too. + Constraints: + Scheduling: + Writers can only proceed if there are no active readers or writers (use semaphore OKToWrite). + Readers can only proceed if there are no active or waiting writers (use semaphore OKToRead). + To keep track of who's reading and writing, need some shared variables. These are called state variables. However, must make sure that only one process manipu- lates state variables at once (use semaphore Mutex). + State variables: + AR = number of active readers. + WR = number of waiting readers. + AW = number of active writers. + WW = number of waiting writers. AW is always 0 or 1. AR and AW may not both be non-zero. + Initialization: + OKToRead = 0; OKToWrite = 0; Mutex = 1; + AR = WR = AW = WW = 0; + Scheduling: writers get preference. + Reader Process: P(Mutex); if ((AW+WW) == 0) { V(OKToRead); AR = AR+1; } else WR = WR+1; V(Mutex); P(OKToRead); -- read the necessary data; P(Mutex); AR = AR-1; if (AR==0 && WW>0) { V(OKToWrite); AW = AW+1; WW = WW-1; } V(Mutex); + Writer Process: P(Mutex); if ((AW+AR+WW) == 0) (do we need WW?) { V(OKToWrite); AW = AW+1; } else WW = WW+1; V(Mutex); P(OKToWrite); -- write the necessary data; P(Mutex); AW = AW-1; if (WW>0) { V(OKToWrite); AW = AW+1; WW = WW-1; } else while (WR>0) { V(OKToRead); AR = AR+1; WR = WR-1; } V(Mutex); + Go through several examples: + Reader enters and leaves system. + Writer enters and leaves system. + Two readers enter system. + Writer enters system and waits. + Reader enters system and waits. + Readers leave system, writer continues. + Writer leaves system, last reader continues and leaves. + Questions: + In case of conflict between readers and writers, who gets priority? + Is the WW necessary in the writer's first if? + Can OKToRead ever get greater than 1? What about OK- ToWrite? + Is the first writer to execute P(Mutex) guaranteed to be the first writer to access the data? Dining Philosophers Problem + Assume 5 philosphers (works for N). Out to dinner at Chinese restaurant. Seated at circular table, with one chopstick between each pair of philosophers. Philosophers need 2 chopsticks to eat. + Assume solution must be: + Symetric - all philosophers use same algorithm + Can't number the philosophers as part of the solution. (can refer to them with numbers) + Efficient - more than one philosopher eats + No central control. + Obvious solution: + (a) pick up left chopstick + (b) pick up right chopstick; wait if necessary, + (c) eat. + Fails, due to immediate deadlock - each philosopher ends up with one chopstick. Each is waiting for right chopstick. + Second solution: + (a) pick up left chopstick, + (b) if available, pick up right chopstick, else, + (b1) put down left chopstick, + (b2) wait for right chopstick, + (b3) pick up right chopstick, + (b4) pick up left chopstick; wait if necessary, + (c) eat. + Fails, in opposite order - now each is waiting for left chopstick. + Solution: N philosophers semaphore mutex (init 1) used for mutual exclusion in access to variables array H(0:N), init `not hungry' values `not hungry, hungry, eating' semaphore array prisem(0:N), init (0) ("private semaphore") procedure test(me): if H((left)~=eating and H(me)=hungry and H(right) ~= eating do begin H(me):=eating V(prisem(me)) end cycle begin think (philosopher me) P(mutex) H(me):=hungry; test(me) V(mutex) P(prisem(me)) eat P(mutex) H(me):=not hungry test(left) test(right) V(mutex) end + Idea here is that a private semaphore is used to control the progress of each process, and a common semaphore is used to allow for unambiguous inspection and modification of common state variables. + Solution is free of deadlock, but permits unbounded delay. A given philospher can starve to death, depending on the sequence of who eats. + Threads (read section 4.5, in Silberschatz and Galvin, 5.1-5.3 in Silberschatz, Galvin and Gagne, and Birrell paper (#4) in readings + Thread, also called a lightweight process, is a type of process. + Thread has its own program counter, register set values, and stack. + Thread shares with 1 or more threads its code, data, and OS resources such as open files. + Task consists of the set of threads sharing code, data, etc. A task with one thread is an ordinary (heavy weight) process. + Switching between threads is much lower overhead than switching between separate processes. Only need to reload user registers, not change entire PCB (e.g. accounting info, etc.). + In some cases, thread switching can be done by code in user-level libraries, so no OS call is required. This is much(!) lower overhead. + Note that if thread switching is done by user, then OS doesn't know about it. Therefore, if one thread is blocked by OS, all are blocked. Also, OS will allocate time per task, even though it may have many threads. + A thread can create child threads of its own. + Note that since memory is shared, there is low overhead sharing, but no protection. + Why Use Threads?: + On Uniprocessor, may provide more convenient model for programming than normal sequential program. (Does not inherantly provide higher efficiency.) + On (shared memory) Multiprocessor, may provide parallelism, since different threads can run on different processors in parallel. + Note that OS must do scheduling if multiprocessors involved (at least to set them up and running on each processor.) + Lower overhead (task switching, memory sharing) than separate parallel (heavyweight) processes.