210 likes | 346 Views
Software Systems Advanced Synchronization. Emery Berger and Mark Corner University of Massachusetts Amherst. Why Synchronization?. Synchronization serves two purposes: Ensure safety for shared updates Avoid race conditions Coordinate actions of threads Parallel computation
E N D
Software SystemsAdvanced Synchronization Emery Berger and Mark Corner University of Massachusetts Amherst
Why Synchronization? • Synchronization serves two purposes: • Ensure safety for shared updates • Avoid race conditions • Coordinate actions of threads • Parallel computation • Event notification • ALL interleavings must be correct • there are lots of interleavings of events • also constrain as little as possible
Synch. Operations • Safety: • Locks provide mutual exclusion • Coordination: • Condition variables provide ordering
Safety • Multiple threads/processes • access shared resource simultaneously • Safe only if: • All accesses have no effect on resource,e.g., reading a variable, or • All accesses idempotent • E.g., a = abs(x), a = highbit(a) • Only one access at a time: mutual exclusion
“The too much milk problem” Model of need to synchronize activities Safety: Example
thread A if (no milk && no note) leave note buy milk remove note Why You Need Locks thread B if (no milk && no note) leave note buy milk remove note too much milk • Does this work?
Mutual Exclusion • Prevent more than one thread from accessing critical section • Serializes access to section • Lock, update, unlock: • lock (&l); • update data; /* critical section */ • unlock (&l);
Too Much Milk: Locks thread A lock(&l) if (no milk) buy milk unlock(&l) thread B lock(&l) if (no milk) buy milk unlock(&l)
What data is shared? • Some data is shared • code, data, heap • Each thread has private data • Stack, SP, PC • All access to shared data must be safe
Exercise! • Simple multi-threaded program • N = number of iterations • Spawn that many threads to compute • value = expensiveComputation(i) • Add value (safely!) to total • Use: • pthread_mutex_init, _lock, _unlock • pthread_mutex_t myLock; • pthread_create, pthread_join • pthread_t threads[N];
Prototypes pthread_t theseArethreads[100]; pthread_mutex_t thisIsALock; typedef void * fnType (void *); pthread_create (pthread_t *, NULL, fnType, void *); pthread_join (pthread_t, void *); pthread_mutex_init (pthread_mutex_t *, NULL) pthread_mutex_lock (pthread_mutex_t *) pthread_mutex_unlock (pthread_mutex_t *)
Solution int main (int argc, char * argv[]) { int n = atoi(argv[1]); // mutex init pthread_mutex_init (&lock); // allocate threads pthread_t * threads = new pthread_t[n]; // spawn threads for (int i = 0; i<n; i++) { // heap allocate args int * newI = new int; *newI = i; pthread_create (&threads[i], NULL, wrapper, (void *) newI); } // join for (int i = 0; i<n; i++) { pthread_join (threads[i], NULL); } // done printf (“total = %d\n”, total); return 0; } #include <pthread.h> int total = 0; pthread_mutex_t lock; void * wrapper (void * x) { int v = *((int *) x); delete ((int *) x); int res = expComp (v); pthread_mutex_lock (&lock); total += res; pthread_mutex_unlock (&lock); return NULL; }
Synch. Operations • Safety: • Locks provide mutual exclusion • Coordination: • Condition variables provide ordering 13
Synch Problem: Queue • Suppose we have a thread-safe queue • insert(item), remove(), empty() • must protect access with locks • Options for remove when queue empty: • Return special error value (e.g., NULL) • Throw an exception • Wait for something to appear in the queue
Three Possible Solutions • Spin • Works? • Could release lock • Works? • Re acquire Lock lock(); while(empty()) {} unlock(); v = remove(); unlock(); while(empty()) {} lock(); v = remove(); • lock() • while (empty()) { • unlock(); • lock(); • } • V = remove(); • unlock();
Solution: Sleep! • Sleep = • “don’t run me until something happens” • What about this? • Cannot hold lock while sleeping! • Dequeue(){ • lock(); • if (queue empty) { • sleep(); • } • take one item; • unlock(); • } • Enqueue(){ • lock(); • insert item; • if (thread waiting) • wake up dequeuer(); • unlock(); • }
Quick Exercise • Does this work? • Dequeue(){ • lock(); • if (queue empty){ • unlock(); • sleep(); • remove item; • } • else unlock; • } • Enqueue(){ • lock(); • insert item; • if (thread waiting) • wake up dequeuer(); • unlock(); • } • No! • deq releases lock, then enq looks for sleeping thread
Condition Variables • Make it possible/easy to go to sleep • Atomically: • release lock • put thread on wait queue • go to sleep • Each cv has a queue of waiting threads • Worry about threads that have been put on the wait queue but have NOT gone to sleep yet? • no, because those two actions are atomic • Each condition variable associated with one lock
Condition Variables • Wait for 1 event, atomically release lock • wait(Lock& l, CV& c) • If queue is empty, wait • Atomically releases lock, goes to sleep • You must be holding lock! • May reacquire lock when awakened (pthreads do) • signal(CV& c) • Insert item in queue • Wakes up one waiting thread, if any • broadcast(CV& c) • Wakes up all waiting threads • Monitors = locks + condition variables • Sometimes combined with data structures
Condition Variable Exercise • Implement “Producer Consumer” • One thread enqueues, another dequeues • void * consumer (void *){ • while (true) { • pthread_mutex_lock(&l); • while (q.empty()){ • pthread_cond_wait(&nempty, &l); • } • cout << q.pop_back() << endl; • pthread_mutex_unlock(&l); • } • } • void * producer(void *){ • while (true) { • pthread_mutex_lock(&l); • q.push_front (1); • pthread_cond_signal(&nempty); • pthread_mutex_unlock(&l); • } • } • Two Questions? • Can I use if instead of while (to check cond)? • Can I signal after unlock?
Bounded-Buffer Exercise • Implement “Producer Consumer” • One thread enqueues, another dequeues • void * consumer (void *){ • while (true) { • pthread_mutex_lock(&l); • while (q.empty()){ • pthread_cond_wait(&nempty, &l); • } • cout << q.pop_back() << endl; • pthread_cond_signal(&nfull); • pthread_mutex_unlock(&l); • } • } • void * producer(void *){ • while (true) { • pthread_mutex_lock(&l); • while (q.size() == N) { • pthread_cond_wait(&nfull,&l); • } • q.push_front (1); • pthread_cond_signal(&nempty); • pthread_mutex_unlock(&l); • } • }