W4118 Operating Systems
310 likes | 333 Vues
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.
W4118 Operating Systems
E N D
Presentation Transcript
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