Download presentation
Presentation is loading. Please wait.
Published byBernice Jackson Modified over 9 years ago
1
Operating Systems ECE344 Ashvin Goel ECE University of Toronto Synchronization
2
Overview Producer-consumer problem Monitors Semaphores Classic synchronization examples 2
3
Synchronization Recall that concurrent programming raises two issues Race conditions o Problem: Certain interleavings cause bad results o Soln: Mutex locks help avoid races by running code atomically Synchronization o Problem: Threads need to synchronize with each other o Soln: Now we consider how to handle synchronization, using a classic synchronization problem in operating systems 3
4
Producer-Consumer Problem Threads communicate with each other using a shared buffer of fixed size (i.e., bounded buffer) o One or more producers fill buffer o One or more consumers empty buffer Two synchronizations conditions o Producers wait if the buffer is full o Consumers wait if the buffer is empty 4
5
Bounded Buffer Implementation Implementation uses a circular buffer o Producers write at in, increment in, go clockwise o Consumers read from out, increment out, go clockwise o Number of elements in buffer: count = (in - out + n) % n o Buffer is full when it has n-1 elements (why not n elements?) count == (n - 1) o Buffer is empty when it has no elements in == out 5 shared variables: char buf[8]; // 7 slots usable int in; // place to write int out; // place to read 7 0 1 2 in = 6 out = 3 buffer has 3 elements
6
Try 1 : Single Producer-Consumer Is this code correct for a single producer, single consumer? 6 char receive() { while (in == out) { } // empty msg = buf[out]; out = (out + 1) % n; return msg; } void send(char msg) { int count = (in – out + n) % n; while (count == n - 1) { } // full buf[in] = msg; in = (in + 1) % n; }
7
Try 2: Single Producer-Consumer Is this code correct for a single producer, single consumer? Is this code correct with multiple producers, multiple consumers? 7 char receive() { while (in == out) { } // empty msg = buf[out]; out = (out + 1) % n; return msg; } void send(char msg) { while ((in–out+n)%n == n - 1) { } // full buf[in] = msg; in = (in + 1) % n; }
8
Try 3 – Use Locking Let’s try to make this code work for multiple producers and consumers by adding locks Is this code correct? 8 char receive() { lock(l); while (in == out) { } // empty msg = buf[out]; out = (out + 1) % n; unlock(l); return msg; } void send(char msg) { lock(l); while ((in–out+n)%n == n - 1) { } // full buf[in] = msg; in = (in + 1) % n; unlock(l); }
9
Try 4 – Release Locks Before Spinning Is this code correct? 9 char receive() { lock(l); while (in == out) { unlock(l); lock(l); } // empty msg = buf[out]; out = (out + 1) % n; unlock(l); return msg; } void send(char msg) { lock(l); while ((in–out+n)%n == n - 1) { unlock(l); lock(l); } // full buf[in] = msg; in = (in + 1) % n; unlock(l); }
10
Is this code correct? o What if we switch unlock() and thread_sleep()? char receive() { lock(l); while (in == out) { unlock(l); thread_sleep(empty); lock(l); } // empty msg = buf[out]; if ((in–out+n)%n == n–1) thread_wakeup(full); out = (out + 1) % n; unlock(l); return msg; } void send(char msg) { lock(l); while ((in–out+n)%n == n - 1) { unlock(l); thread_sleep(full); lock(l); } // full buf[in] = msg; if (in == out) thread_wakeup(empty); in = (in + 1) % n; unlock(l); } Try 5 – Sleep After Unlocking 10
11
Synchronization Challenges Can’t spin or sleep while holding lock o Causes deadlock Can’t release lock and then sleep o Causes race because wakeup notification can be lost Need a way to release the lock and sleep atomically! o Next we see how this can be done using monitors 11
12
Monitors A structured method for concurrent programming Mutual exclusion o Any shared data is accessed via methods o All methods acquire lock at the start of their code, release lock at the end of their code o Thus all shared data is accessed in critical section Synchronization o Methods synchronize with each other using one or more condition variables o These variables allow programs to define arbitrary conditions under which: A thread goes to sleep (blocks) A thread wakes up another sleeping thread 12
13
Condition Variable Abstraction A condition variable is used within monitor methods o cv = cv_create():// create a condition variable o cv_destroy(cv): // destroy a condition variable o cv_wait(cv, lock): Always wait on some condition until another thread signals it While thread waits, lock is released atomically When wait returns, lock is reacquired o cv_signal(cv, lock): Wakeup one thread waiting on the condition If a signal occurs before a wait, signal is lost o cv_broadcast(cv, lock): Wakeup all threads waiting on the condition 13
14
Producer-Consumer with Monitors 14 char receive() { lock(l); while (in == out) { wait(empty, l); } // empty msg = buf[out]; if ((in–out+n)%n == n–1) signal(full, l); out = (out + 1) % n; unlock(l); return msg; } void send(char msg) { lock(l); while ((in–out+n)%n == n - 1) { wait(full, l); } // full buf[in] = msg; if (in == out) signal(empty, l); in = (in + 1) % n; unlock(l); } Global variables: buf[n], in, out; lock l = 0; cv full; // no initialization cv empty;
15
Variable Initialization Using Monitors Is this code correct? 15 // called by Thread T1 // initially V = NULL Method1() { lock(l); V = malloc(…); // signal that V // is non-NULL signal(cv, l); … unlock(l); } // called by Thread T2 Method2() { lock(l); // wait until V is non-NULL wait(cv, l); assert(V); … unlock(l); }
16
Variable Initialization Using Monitors Note that wait and signal must be called within lock What would happen if the lock was not used above? 16 // called by Thread T1 // initially V = NULL Method1() { lock(l); V = malloc(…); // signal that V // is non-NULL signal(cv, l); … unlock(l); } // called by Thread T2 Method2() { lock(l); if (!V) { // wait until V is non-NULL wait(cv, l); } assert(V); … unlock(l); }
17
Semaphores Semaphores provide an alternate method for synchronization A semaphore tracks number of available resources using down() and up() operations 17 down(semaphore s) { while (s <= 0) { // wait until resource is available } s = s – 1; // acquire a resource // after down(), s >= 0 } up(semaphore s) { s = s + 1; // make a resource available }
18
Understanding Semaphores Why must down and up be atomic? If s is initialized to 1, then o down(s) behaves like lock(s) o up(s) behaves like unlock(s) The differences between semaphores and locks are based on the intended use o Different threads call down() and up() for synchronization o up() can be called before down(), to “bank” resources 18
19
Synchronizing With Interrupts Consider a program that reads keys from the keyboard o Program needs to wait until a key is typed, or get the key that has already been typed o Keyboard interrupt handler can buffer one key in key_buf o keyboard_sem tracks nr. of keys available, initialized to 0 19 // on keyboard interrupt void kbd_interrupt_handler(char key) { key_buf = key; up(keyboard_sem); } // program issues read char read_from_keyboard() { down(keyboard_sem); return key_buf; }
20
Producer-Consumer with Semaphores 20 char receive() { down(full); lock(l); msg = buf[out]; out = (out + 1) % n; unlock(l); up(empty); return msg; } void send(char msg) { down(empty); lock(l); buf[in] = msg; in = (in + 1) % n; unlock(l); up(full); } Global variables: buf[n], in, out; lock l; sem full = 0; // no full slots sem empty = n; // all slots are empty
21
Implementing Blocking Semaphores Previous semaphore implementation used spinning A blocking semaphore requires interacting with thread scheduler o Using thread_sleep() and thread_wakeup() 21 struct semaphore { int count; …}; down(sem) { while (sem->count <= 0) { thread_sleep(sem); } sem->count--; } up(sem) { sem->count++; thread_wakeup(sem); }
22
Atomic Semaphore Operations down(s) and up(s) must be atomic How should they be implemented? o Mutual exclusion (as usual) Disable interrupts on uniprocessors Use spinlocks on multi-processors Next, we show the implementation for a uniprocessor 22
23
Implementing down() and up() Atomically Could we replace the “while” in down() with “if”? Why does down() disable interrupts before calling thread_sleep()? Isn’t sleeping while holding lock (interrupt disable) a problem? 23 down(sem) { disable interrupts; while (sem->count <= 0) { thread_sleep(sem); } sem->count--; enable interrupts; } up(sem) { disable interrupts; s->count++; thread_wakeup(sem); enable interrupts; }
24
Classic Synchronization Examples Dining philosophers Readers and writers 24
25
The Dining Philosophers Problem Five philosophers sit at a table A fork lies between every pair of philosophers Philosophers (1) think, (2) grab one fork, (3) grab another fork, (4) eat, (5) put down one fork, (6) put the other fork 25
26
Dining Philosophers: Try 1 Assume take_fork and put_fork have locks in them to make them atomic o Is this solution correct? 26 #define N 5 Philosopher() { while(TRUE) { Think(); take_fork(i); take_fork((i+1)% N); Eat(); put_fork(i); put_fork((i+1)% N); } Each philosopher is modeled with a thread
27
Dining Philosophers: Try 2 27 #define N 5 Philosopher() { while(TRUE) { Think(); take_fork(i); take_fork((i+1)% N); Eat(); put_fork(i); put_fork((i+1)% N); } take_forks(i) put_forks(i)
28
Take Forks 28 // test whether philosopher i // can take both forks // call with mutex set – why? test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING){ state[i] = EATING; // Signal Philosopher i up(sem[i]); } int state[N]; lock mutex; sem sem[N] = {0}; take_forks(int i) { lock(mutex); state[i] = HUNGRY; test(i); unlock(mutex); down(sem[i]); }
29
Put Forks 29 // test whether philosopher i // can take both forks // call with mutex set test(int i) { if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING){ state[i] = EATING; // Signal Philosopher i up(sem[i]); } put_forks(int i) { lock(mutex); state[i] = THINKING; test(LEFT); test(RIGHT); unlock(mutex); } int state[N]; lock mutex; sem sem[N] = {0};
30
The Readers and Writers Problem Multiple reader and writer threads want to access some shared data Multiple readers can read concurrently Writers must synchronize with readers and other writers Synchronization requirements o Only one writer can write the shared data at a time o When a writer is writing, no readers must access the data Goals o Maximize concurrency o Prevent starvation 30
31
Readers/Writers - Basics 31 Reader () { rc = rc + 1; // Read shared data rc = rc – 1; // non-critical section } Mutex lock = UNLOCKED; Semaphore data = 1; int rc = 0; Writer () { // non-critical section // Write shared data }
32
Readers/Writers - Mutex 32 Reader () { lock(lock); rc = rc + 1; unlock(lock); // Read shared data lock(lock); rc = rc – 1; unlock(lock); // non-critical section } Mutex lock = UNLOCKED; Semaphore data = 1; int rc = 0; Writer () { // non-critical section // Write shared data }
33
Readers/Writers – Synchronization Any problems with this solution? 33 Reader () { lock(lock); if (rc == 0) down(data); rc = rc + 1; unlock(lock); // Read shared data lock(lock); rc = rc – 1; if (rc == 0) up(data); unlock(lock); // non-critical section } Mutex lock = UNLOCKED; Semaphore data = 1; int rc = 0; Writer () { // non-critical section down(data); // Write shared data up(data); }
34
Summary Synchronization enables threads to wait on some condition before proceeding Producer-consumer problem o Motivates the need for synchronization o Show that implementing synchronization using ad-hoc methods generally leads to incorrect results Two systematic solutions for implementing synchronization o Monitors o Semaphores 34
35
Think Time What is the difference between mutual exclusion and synchronization? Why are locks, by themselves, not sufficient for solving synchronization problems? How would you solve the producer-consumer problem using interrupt disabling What are the differences between a monitor and a semaphore? What are the differences between wait() and down()? What are the differences between signal() and up()? Why might you prefer one over the other? 35
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.