CS 5204 Operating Systems Semaphores Condition Variables Godmar
CS 5204 Operating Systems Semaphores & Condition Variables Godmar Back
MULTI-THREADING CS 5204 Fall 2012
Coordinating Multiple Threads • Aside from coordinating access to shared items, threads may need to communicate about events – “has event A already happened in another thread? ” – aka “precedence constraint”, or “scheduling constraint” • Do B after A • Must do so – Correctly (never miss that event A has occurred when in fact it has) – Efficiently • Don’t waste resources in the process • Don’t unnecessarily delay notification of event A CS 5204 Fall 2012
int coin_flip; static void * thread 1(void *_) { coin_flip = rand() % 2; printf("Thread 1: flipped coin %dn", coin_flip); return NULL; } static void * thread 2(void *_) { printf("Thread 2: flipped coin %dn", coin_flip); return NULL; } int main() { int i, N = 2; pthread_t t[N]; srand(getpid()); pthread_create(&t[1], NULL, thread 2, NULL); pthread_create(&t[0], NULL, thread 1, NULL); for (i = 0; i < N; i++) pthread_join(t[i], NULL); return 0; } Q. : How can thread 2 make sure that ‘coin_flip’ has occurred before printing its outcome? CS 5204 Fall 2012
int coin_flip; volatile bool coin_flip_done; static void * thread 1(void *_) { coin_flip = rand() % 2; coin_flip_done = true; printf("Thread 1: flipped coin %dn", coin_flip); return NULL; } Thread 2 could “busywait” – spin until thread 1 completes the coin flip. Exceptions not withstanding, this is practically never an acceptable solution. The somewhat less wasteful static void * thread 2(void *_) variant of busy-waiting: { while (!coin_flip_done) /* Thread 2 spins, "busy-waits" until the coin flip is done. sched_yield(); * This is an unacceptable solution. Bad for the planet, too. */ is not acceptable, either. while (!coin_flip_done) continue; printf("Thread 2: flipped coin %dn", coin_flip); return NULL; } CS 5204 Fall 2012 -wastes CPU cycles - is fragile (volatile needed when using –O) - does not document semantics
Semaphores Source: inter. scoutnet. org • Invented by Edsger Dijkstra in 1960 s • Counter S, initialized to some value, with two operations: – P(S) or “down” or “wait” – if counter greater than zero, decrement. Else wait until greater than zero, then decrement – V(S) or “up” or “signal” or “post” – increment counter, wake up any threads stuck in P. • Semaphores don’t go negative: – #V + Initial. Value - #P >= 0 • Note: direct access to counter value after initialization is not allowed • Counting Semaphores vs Binary Semaphores – Binary: counter can only be 0 or 1 • Simple to implement, yet powerful – Can be used for many synchronization problems CS 5204 Fall 2012
int coin_flip; sem_t coin_flip_done; // semaphore for thread 1 to signal coin flip static void * thread 1(void *_) POSIX Semaphores { coin_flip = rand() % 2; sem_post(&coin_flip_done); // raise semaphore, increment, 'up' printf("Thread 1: flipped coin %dn", coin_flip); Notice the 3 rd argument of } sem_init() – it gives the initial value of the semaphore: ‘ 0’ means the semaphore is used to static void * int main() express scheduling constraint thread 2(void *_) { { … // wait until semaphore is raised, sem_init(&coin_flip_done, 0, 0); // then decrement, 'down' pthread_create(&t[1], NULL, thread 2, NULL); sem_wait(&coin_flip_done); pthread_create(&t[0], NULL, thread 1, NULL); printf("Thread 2: flipped coin %dn", … coin_flip); } } CS 5204 Fall 2012
Implementing Mutual Exclusion with Semaphores • Semaphores can be used to build locks • Must initialize semaphore with 1 to allow one thread to enter critical section • This is not a recommended style, despite of what Bryant & O’Hallaron suggest – you should use a mutex instead [Cantrill & Bonwick 2008] sem_t S; sem_init(&S, 0, 1); lock_acquire() { // try to decrement, wait if 0 sem_wait (S); } lock_release() { // increment (wake up waiters if any) sem_post(S); } • Easily generalized to allow at most N simultaneous threads: multiplex pattern (i. e. , a resource can be accessed by at most N threads) CS 5204 Fall 2012
Condition Variables - Intro • Besides (and perhaps more so) than semaphores, condition variables are another widely used form to implement ‘signaling’ kinds of coordination/synchronization – In POSIX Threads, Java, C# • Based on the concept of a Monitor – ADT that combines protected access to state and signaling • Confusing terminology alert: – Word ‘signal’ is overloaded 3 times • Semaphore signal (V(), “up”, “post”) • Monitor/Condition variable signal (“signal”, “notify”) • Unix signals – Word ‘wait’ is overloaded • Semaphore wait (P(), “down”) • Monitor/Condition variable wait • Unix wait() for child process CS 5204 Fall 2012
Monitors • A monitor combines a set of shared variables & operations to access them – Think of a Java class with no public fields & all public methods carrying the attribute ‘synchronized’ • A monitor provides implicit synchronization (only one thread can access private variables simultaneously) – Single lock is used to ensure all code associated with monitor is within critical section • A monitor provides a general signaling facility – Wait/Signal pattern (similar to, but different from semaphores) – May declare & maintain multiple signaling queues CS 5204 Fall 2012
Monitors (cont’d) • Classic monitors are embedded in programming languages – Invented by Hoare & Brinch-Hansen 1972/73 – First used in Mesa/Cedar System @ Xerox PARC 1978 – Adapted version available in Java/C# • (Classic) Monitors are safer than semaphores – can’t forget to lock data – compiler checks this • In contemporary C, monitors are a synchronization pattern that is achieved using locks & condition variables – Helps to understand monitor abstraction to use it correctly CS 5204 Fall 2012
Infinite Buffer w/ Monitor monitor buffer { /* implied: struct lock mlock; */ private: char buffer[]; int head, tail; public: produce(item); item consume(); } buffer: : produce(item i) { /* try { lock_acquire(&mlock); */ buffer[head++] = i; /* } finally {lock_release(&mlock); } */ } buffer: : consume() { /* try { lock_acquire(&mlock); */ return buffer[tail++]; /* } finally {lock_release(&mlock); } */ } • Monitors provide implicit protection for their internal variables – Still need to add the signaling part CS 5204 Fall 2012
Condition Variables • Used by a monitor for signaling a condition – a general (programmer-defined) condition, not just integer increment as with semaphores – Somewhat weird: the condition is actually not stored in the variable – it’s typically some boolean predicate of monitor variables, e. g. “buffer. size > 0” – the condition variable itself is better thought of as a signaling queue • Monitor can have more than one condition variable • Three operations: – Wait(): leave monitor, wait for condition to be signaled, reenter monitor – Signal(): signal one thread waiting on condition – Broadcast(): signal all threads waiting on condition CS 5204 Fall 2012
Condition Variables as Queues Enter Wait Signal – Wait(): adds current thread to (end of queue) & block Wait – Signal(): pick one thread from queue & unblock it Signal – Broadcast(): unblock all threads Note on style: best practice is to leave monitor only once, and near the procedure’s entry. CS 5204 Fall 2012 Region of mutual exclusion • A condition variable’s state is just a queue of waiters: Exit
Bounded Buffer w/ Monitor monitor buffer { condition items_avail; condition slots_avail; private: char buffer[]; int head, tail; public: produce(item); item consume(); } buffer: : produce(item i) { while ((tail+1–head)%CAPACITY==0) slots_avail. wait(); buffer[head++] = i; items_avail. signal(); } buffer: : consume() { while (head == tail) items_avail. wait(); item i = buffer[tail++]; slots_avail. signal(); return i; } CS 5204 Fall 2012
Bounded Buffer w/ Monitor monitor buffer { condition items_avail; condition slots_avail; private: char buffer[]; int head, tail; public: produce(item); item consume(); } Q 1. : How is lost update problem avoided? Q 2. : Why while() and not if()? buffer: : produce(item i) { while ((tail+1–head)%CAPACITY==0) slots_avail. wait(); buffer[head++] = i; items_avail. signal(); } buffer: : consume() { while (head == tail) items_avail. wait(); item i = buffer[tail++]; lock_release(&mlock); slots_avail. signal(); block_on(items_avail); return i; lock_acquire(&mlock); } CS 5204 Fall 2012
cond_signal semantics • cond_signal keeps lock, so it leaves signaling thread in monitor • waiter is made READY, but can’t enter until signaler gives up lock • There is no guarantee whether signaled thread will enter monitor next or some other thread will (who may be already waiting!) – so must always use “while()” when checking condition – cannot assume that condition set by signaling thread will still hold when monitor is reentered by signaled thread • This semantics is also referred to as “Mesa-Style” after the system in which it was first used – POSIX Threads, Java, and C# use this semantics CS 5204 Fall 2012
Condition Variables vs. Semaphores • Condition Variables – Signals are lost if nobody’s on the queue (e. g. , nothing happens) – Wait() always blocks • Semaphores – Signals (calls to V() or sem_post()) are remembered even if nobody’s current waiting – Wait (e. g. , P() or sem_wait()) may or may not block CS 5204 Fall 2012
Monitors in C • POSIX Threads as well as many custom environments • No compiler support, must do it manually – must declare locks & condition vars – must call pthread_mutex_lock/unlock when entering & leaving the monitor – must use pthread_cond_wait/pthread_cond_signal to wait for/signal condition • Note: pthread_cond_wait(&c, &m) takes monitor lock as parameter – necessary so monitor can be left & reentered without losing signals • pthread_cond_signal() does not – leaving room for programmer error! CS 5204 Fall 2012
Monitors in Java • synchronized block means – enter monitor – execute block – leave monitor • wait()/notify() use condition variable associated with receiver – Every object in Java can function as a condition variable (just like it can function as a lock) – More restrictive than Pthreads/C which allow multiple condition variables (signaling conditions) to be used in connection with a lock protecting state class buffer { private char buffer[]; private int head, tail; public synchronized produce(item i) { while (buffer_full()) this. wait(); buffer[head++] = i; this. notify. All(); } public synchronized item consume() { while (buffer_empty()) this. wait(); i = buffer[tail++]; this. notify. All(); return ; } } CS 5204 Fall 2012
Monitors in Java, Take 2 • Previous slide (bounded buffer) is actually an example of where Java’s built-in monitors suck – Needed “notify. All()” to make sure one at least one of the right kind of threads was woken up – Unacceptably inefficient • Use java. util. concurrent. locks. Condition instead in cases where multiple condition queues are needed import java. util. concurrent. locks. *; class buffer { private Reentrant. Lock monitorlock = new Reentrant. Lock(); private Condition items_available = monitorlock. new. Condition(); private Condition slots_available = monitorlock. new. Condition(); public /* NO SYNCHRONIZED here */ void produce(item i) { monitorlock(); try { while (buffer_full()) slots_available. await(); buffer[head++] = i; items_available. signal(); } finally { monitorlock. unlock(); } } /* consume analogous */ } CS 5204 Fall 2012
A Read. Write Lock Implementation struct lock mlock; // protects rdrs & wrtrs void write_lock_acquire() { int readers = 0, writers = 0; lock_acquire(&mlock); struct condvar canread, canwrite; while (readers > 0 || writers > 0) void read_lock_acquire() { cond_wait(&canwrite, &mlock); lock_acquire(&mlock); writers++; while (writers > 0) lock_release(&mlock); cond_wait(&canread, &mlock); } readers++; lock_release(&mlock); void write_lock_release() { } lock_acquire(&mlock); void read_lock_release() { writers--; lock_acquire(&mlock); ASSERT(writers == 0); if (--readers == 0) cond_broadcast(&canread); cond_signal(&canwrite); lock_release(&mlock); } } Note: this is a naïve implementation that may lead to livelock – no guarantees a reader or writer ever enters the locked section even if every threads eventually leaves it. CS 5204 Fall 2012
Summary • Semaphores & Condition Variables provide signaling facilities – Condition variables are loosely based on “monitor” concept • Java/C# provide syntactic sugar • Semaphores have “memory” – But require that # of signals matches # of waits – Good for rendezvous, precedence constraints – if problem lends itself to semaphore, use one • Always use idiomatic “while (!cond) *_wait()” pattern when using condition variables (in C) or Object. wait() (in Java) CS 5204 Fall 2012
- Slides: 23