Class 5 CS 170 13 April 2020 On the board ------------ 1. Last time 2. Condition variables --------------------------------------------------------------------------- 1. Last time --threads: why do we have them? --oftentimes a natural way to structure a computing task --once we have threads, we have to worry about *concurrent access to shared memory*: multiple execution contexts modifying the same memory at the same time --because there are multiple CPUs (with threads running on those CPUs) --or because the instructions from different threads can be interleaved on one CPU, owing to scheduling --this state of affairs has to be controlled: *synchronization*. --accesses of shared variables (for example, "count" in the bounded buffer example) are said to be in a _critical section_ --Critical sections are protected from concurrent execution --classic synchronization primitive: the mutex. --> we have told you about the *interface* to mutexes (how they are used). we haven't said much about their *implementation*; we will cover that next week. --you may at this point have an intuition for mutexes (they guarantee mutual exclusion, which sounds good), but what are they really accomplishing? --*atomicity* is required if you want to reason about code without contorting your brain to reason about all possible interleavings --atomicity requires mutual exclusion aka a solution to critical sections --mutexes provide that solution --once you have mutexes, don't have to worry about arbitrary interleavings. critical sections are interleaved, but those are much easier to reason about than individual operations. --why? because of _invariants_. examples of invariants: "list structure has integrity" "'count' reflects the number of entries in the buffer" the meaning of lock.acquire() is that if and only if you get past that line, it's safe to violate the invariants. the meaning of lock.release() is that right _before_ that line, any invariants need to be restored. the above is abstract. let's make it concrete: invariant: "list structure has integrity" so protect the list with a mutex only after acquire() is it safe to manipulate the list --why aren't we worried about *processes* trashing each other's memory? (because the OS, with the help of the hardware, arranges for two different processes to have isolated memory space. such isolation is one of the uses of virtual memory, which we will study in a few weeks.) --[work through producer consumer problem on the board (from the handout of the previous lecture) --what can go wrong without the use of synchronization primitives? --how are synchronization primitives used? ] 2. Condition variables A. Motivation --producer/consumer queue --very common paradigm. also called "bounded buffer": --producer puts things into a shared buffer --consumer takes them out --producer must wait if buffer is full; consumer must wait if buffer is empty --shows up everywhere --Soda machine: producer is delivery person, consumer is soda drinkers, shared buffer is the machine --OS implementation of pipe() --DMA buffers --producer/consumer queue using mutexes --what's the problem with that? --answer: a form of *busy waiting* --It is convenient to break synchronization into two types: --*mutual exclusion*: allow only one thread to access a given set of shared state at a time --*scheduling constraints*: wait for some other thread to do something (finish a job, produce work, consume work, accept a connection, get bytes off the disk, etc.) B. Usage --API --void cond_init (Cond *, ...); --Initialize --void cond_wait(Cond *c, Mutex* m); --Atomically unlock m and sleep until c signaled --Then re-acquire m and resume executing --void cond_signal(Cond* c); --Wake one thread waiting on c [in some pthreads implementations, the analogous call wakes *at least* one thread waiting on c. Check the the documentation (or source code) to be sure of the semantics. But, actually, your implementation shouldn't change since you need to be prepared to be "woken" at any time, not just when another thread calls signal(). More on this below.] --void cond_broadcast(Cond* c); --Wake all threads waiting on c C. Important points (1) We MUST use "while", not "if". Why? --Because we can get an interleaving like this: --The signal() puts the waiting thread on the ready list but doesn't run it --That now-ready thread is ready to acquire() the mutex (inside cond_wait()). --But a *different* thread (a third thread: not the signaler, not the now-ready thread) could acquire() the mutex, work in the critical section, and now invalidates whatever condition was being checked --Our now-ready thread eventually acquire()s the mutex... --...with no guarantees that the condition it was waiting for is still true --Solution is to use "while" when waiting on a condition variable --DO NOT VIOLATE THIS RULE; doing so will (almost always) lead to incorrect code --NOTE: NOTE: NOTE: There are two ways to understand while-versus-if: (a) It's the 'while' condition that actually guards the program. (b) There's simply no guarantee when the thread proceeds that the condition hods. (2) cond_wait releases the mutexes and goes into the waiting state in one function call --QUESTION: Why? --That is, why does cond_wait need to both release the mutex and sleep? Why not: while (count == BUFFER_SIZE) { release(&mutex); cond_wait(&nonfull); acquire(&mutex); } --Answer: can get stuck waiting. Producer: while (count == BUFFER_SIZE) Producer: release() Consumer: acquire() Consumer: ..... Consumer: cond_signal(&nonfull) Producer: cond_wait(&nonfull) --Producer will never hear the signal!