W4118 Operating Systems Instructor: Junfeng Yang Logistics Homework 2 due time: 3:09 pm this Thursday (one hour before class) Submit everything electronically at courseworks, including written assignment Last lecture Synchronization Layered approach to synchronization Critical section requirements: safe, live, bounded • Desirable: efficient, fair, simple Locks • Uniprocessor implementation: disable and enable interrupts • Software-based locks: peterson’s algorithm • Locks with hardware support – atomic test_and_set Today Lock (wrap up) Semaphore Monitor A classical synchronization problem: read and write lock Recall: Spin-wait or block Spin-lock may waste CPU cycles: lock holder gets preempted, and scheduled threads try to grab lock Shouldn’t use spin-lock on single core On multi-core, good plan is: spin a bit, then yield Problem with simple yield lock() { } Problem: while(test_and_set(&flag)) yield(); Still a lot of context switches; poll for lock Starvation possible Why? No control over who gets the lock next Need explicit control over who gets the lock Implementing locks: version 4 The idea Add thread to queue when lock unavailable In unlock(), wake up one thread in queue lock() { if (flag == 1) add myself to wait queue yield … } Problem I: may lose the wake up unlock() { flag = 0 if(any thread in wait queue) wake up one wait thread … Lock from a third } thread Fix: use a spin_lock or lock w/ simple yield! Doesn’t completely avoid spin-wait, but make wait time short, thus reasonable Problem II: may not wake up the right thread Fix: unlock() directly transfers lock to waiting thread Implementing locks: version 4, the code typedef struct __mutex_t { int flag; // 0: mutex is available, 1: mutex is not available int guard; // guard lock to avoid losing wakeups queue_t *q; // queue of waiting threads } mutex_t; void lock(mutex_t *m) { while (test_and_set(m->guard)) ; //acquire guard lock by spinning if (m->flag == 0) { m->flag = 1; // acquire mutex m->guard = 0; } else { enqueue(m->q, self); m->guard = 0; yield(); } } void unlock(mutex_t *m) { while (test_and_set(m->guard)) ; if (queue_empty(m->q)) // release mutex; no one wants mutex m->flag = 0; else // direct transfer mutex to next thread wakeup(dequeue(m->q)); m->guard = 0; } This is very close to real mutex implementations Today Lock (wrap up) Semaphore Monitor A classical synchronization problem: read and write lock Semaphore Motivation Problem with lock: mutual exclusion, but no ordering; may want more E.g. Producer-consumer problem $ cat 1.txt | sort | uniq | wc Producer: creates a resource Consumer: uses a resource bounded buffer between them Scheduling order: producer waits if buffer full, consumer waits if buffer empty Semaphore Definition A synchronization variable that: Contains an integer value • Can’t access directly • Must initialize to some value – sem_init(sem_t *s, int pshared, unsigned int value) Has two operations to manipulate this integer • sem_wait, or down(), P() (comes from Dutch) • sem_post, or up(), V() (comes from Dutch) int sem_wait(sem_t *s) { wait until value of semaphore s is greater than 0 decrement the value of semaphore s by 1 } int sem_post(sem_t *s) { increment the value of semaphore s by 1 if there are 1 or more threads waiting, wake 1 } Semaphore Uses // initialize to X sem_init(s, 0, X) Mutual exclusion Semaphore as mutex What should initial value be? • Binary semaphore: X=1 • ( Counting semaphore: X>1 ) sem_wait(s); // critical section sem_post(s); Scheduling order One thread waits for another What should initial value be? //thread 0 … // 1st half of computation sem_post(s); // thread 1 sem_wait(s); … //2nd half of computation Producer-Consumer (Bounded-Buffer) Problem Bounded buffer: size ‘N’ Producer process writes data to buffer Access entry 0… N-1, then “wrap around” to 0 again Must not write more than ‘N’ items more than consumer “ate” Consumer process reads data from buffer Should not try to consume if there is no data 0 1 N-1 Producer Consumer Solving Producer-Consumer problem Two semaphores sem_t full; // # of filled slots sem_t empty; // # of empty slots Problem: mutual exclusion? sem_init(&full, 0, 0); sem_init(&empty, 0, N); producer() { sem_wait(empty); … // fill a slot sem_post(full); } consumer() { sem_wait(full); … // empty a slot sem_post(empty); } Solving Producer-Consumer problem: Final Three semaphores sem_t full; // # of filled slots sem_t empty; // # of empty slots sem_t mutex; // mutual exclusion sem_init(&full, 0, 0); sem_init(&empty, 0, N); sem_init(&mutex, 0, 1); producer() { sem_wait(empty); sem_wait(&mutex); … // fill a slot sem_post(&mutex); sem_post(full); } consumer() { sem_wait(full); sem_wait(&mutex); … // empty a slot sem_post(&mutex); sem_post(empty); } How to Implement Semaphores? Part of your next programming assignment Today Lock (wrap up) Semaphore Monitor A classical synchronization problem: read and write lock Monitors Background Concurrent programming meets object-oriented programming When concurrent programming became a big deal, objectoriented programming too People started to think about ways to make concurrent programming more structured Monitor: object with a set of monitor procedures and only one thread may be active (i.e. running one of the monitor procedures) at a time Schematic view of a Monitor Can think of a monitor as one big lock for a set of operations/ methods In other words, a language implementation of mutexes How to Implement Monitor? Compiler automatically inserts lock and unlock operations upon entry and exit of monitor procedures class account { int balance; public synchronized void deposit() { ++balance; } public synchronized void withdraw() { --balance; } }; lock(m); ++balance; unlock(m); lock(m); --balance; unlock(m); Condition Variables Need wait and wakeup as in semaphores Monitor uses Condition Variables Conceptually associated with some conditions Operations on condition variables: wait(): suspends the calling thread and releases the monitor lock. When it resumes, reacquire the lock. Called with condition is not true signal(): resumes one thread (if any) waiting in wait(). Called when condition becomes true broadcast(): resumes all threads waiting in wait() Monitor with Condition Variables Subtle Differences between condition variables and semaphores Semaphores are sticky: they have memory, sem_post() will increment the semaphore, even if no one has called sem_wait() Condition variables are not: if no one is waiting for a signal(), this signal() is not saved Producer-Consumer with Monitors monitor ProducerConsumer { int nfull = 0; cond notfull, notempty; producer() { if (nfull == N) wait (notfull); … // fill a slot ++ nfull; signal (notempty); } }; consumer() { if (nfull == 0) wait (notempty); … // empty a slot -- nfull signal (notfull); } nfull: number of filled buffers Need to do our own counting for condition variables notfull and notempty: two condition variables notfull: not all slots are full notempty: not all slots are empty Condition Variable Semantics Problem: when signal() wakes up a waiting thread, which thread to run inside the monitor, the signaling thread, or the waiting thread? Hoare semantics: suspends the signaling thread, and immediately transfers control to the woken thread Difficult to implement in practice Mesa semantics: signal() moves a single waiting thread from the blocked state to a runnable state, then the signaling thread continues until it exits the monitor Easy to implement Problem: race! E.g. before a woken consumer continues, another consumer comes in and grabs the buffer Fixing the Race in Mesa Monitors monitor ProducerConsumer { int nfull = 0; cond notfull, notempty; producer() { while (nfull == N) wait (notfull); … // fill slot ++ nfull; signal (notempty); } }; consumer() { while (nfull == 0) wait (notempty); … // empty slot -- nfull signal (notfull); } The fix: when woken, a thread must recheck the condition it was waiting on Most systems use mesa semantics E.g. pthread Thus, you should remember to recheck Monitor with pthread class ProducerConsumer { int nfull = 0; pthread_mutex_t m; pthread_cond_t notfull, notempty; public: producer() { pthread_mutex_lock(&m); while (nfull == N) ptherad_cond_wait (&notfull, &m); … // fill slot ++ nfull; pthread_cond_signal (notempty); pthread_mutex_unlock(&m); } … }; C/C++ don’t provide monitors; but we can implement monitors using pthread mutex and condition variable For producer-consumer problem, need 1 pthread mutex and 2 pthread condition variables (pthread_cond_t) Manually lock and unlock mutex for monitor procedures pthread_cond_wait (cv, m): atomically waits on cv and releases m Why atomically? You figure out Today Lock (wrap up) Semaphore Monitor A classical synchronization problem: read and write lock Readers-Writers Problem Courtois et al 1971 Models access to a database A reader is a thread that needs to look at the database but won’t change it. A writer is a thread that modifies the database Example: making an airline reservation When you browse to look at flight schedules the web site is acting as a reader on your behalf When you reserve a seat, the web site has to write into the database to make the reservation Solving Readers-Writers w/ Regular Lock sem_t lock; Writer sem_wait (lock); ... // write shared data ... sem_post (lock); Reader sem_wait (lock); ... // read shared data ... sem_post(lock); Problem: unnecessary synchronization Only one writer can be active at a time However, any number of readers can be active simultaneously ! Solution: Idea: differentiate lock for read and lock for write Readers-Writers Lock int nreader = 0; sem_t lock = 1, write_lock = 1; Writer sem_wait (write_lock); ... // write shared data ... sem_post (write_lock); Problem: may starve writer How to fix? Not that straightforward. You figure out Reader sem_wait (lock); ++ nreader; if (nreader == 1) // first reader sem_wait (write_lock); sem_post (lock); ... // read shared data ... sem_wait (lock); -- nreader; if (nreader == 0) // last reader sem_post (write_lock); sem_post (lock);