310 likes | 327 Views
Explore synchronization techniques like locks, semaphores, and monitors including their implementation methods and classical problems. Learn the importance of layered synchronization and efficient resource allocation strategies.
E N D
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() { while(test_and_set(&flag)) yield(); } • Problem: • 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 • Problem I: may lose the wake up • 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 lock() { if (flag == 1) add myself to wait queue yield … } unlock() { flag = 0 if(any thread in wait queue) wake up one wait thread … } Lock from a third 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; • This is very close to real mutex implementations 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; }
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) sem_wait(s); // critical section sem_post(s); • Mutual exclusion • Semaphore as mutex • What should initial value be? • Binary semaphore: X=1 • ( Counting semaphore: X>1 ) • 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’ • Access entry 0… N-1, then “wrap around” to 0 again • Producer process writes data to buffer • 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, object-oriented 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()
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 • 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 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 (¬full, &m); … // fill slot ++ nfull; pthread_cond_signal (notempty); pthread_mutex_unlock(&m); } … };
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); • 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 Reader sem_wait (lock); . . . // read shared data . . . sem_post(lock);
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); 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); Problem: may starve writer How to fix? Not that straightforward. You figure out