220 likes | 234 Views
Semaphores. Jiwoong Park(jwpark@dcslab.snu.ac.kr) School of Computer Science and Engineering Seoul National University. Overview. Semaphore A synchronization primitive Invented by Edsger Dijkstra Uses Building Synchronization Primitives Binary Semaphores (Locks)
E N D
Semaphores Jiwoong Park(jwpark@dcslab.snu.ac.kr) School of Computer Science and Engineering Seoul National University
Overview • Semaphore • A synchronization primitive • Invented by Edsger Dijkstra • Uses • Building Synchronization Primitives • Binary Semaphores (Locks) • Semaphores as Condition Variables • Solving Problems • Producer/Consumer (Bounded-Buffer) Problem • Reader-Writer Problem • Dining Philosopher’s Problem • Implementation • Zemaphores • Our own version of semaphores using Locks and Condition Variables
Semaphores • Definition • An object with an integer value that can be manipulated with two routines • sem_wait( ) / sem_post( ) in the POSIX standard • Prerequisites • Initialization needed before calling any other routines • sem_init( ): Initializes an unnamed semaphore • Atomicity of the increment, decrement, and comparison operation needed • Atomic read-modify-write operations provided by H/W • Mutual exclusion algorithm provided by S/W
Routines int sem_wait (sem_t *s) { decrement the value of semaphore s by one wait if value of semaphore s is negative } int sem_post (sem_t *s) { increment the value of semaphore s by one if there are one or more threads waiting, wake one } Note that when negative, s->value == # of waiting threads • Routines • sem_wait( ) • a.k.a. P( ) (for the Dutch word “to probe”) • sem_post( ) • a.k.a. V( ) (for the Dutch word “to test”)
Locks (Binary Semaphores) sem_t m; sem_init(&m, 0, 1); // initialize semaphore to 1 sem_wait(&m); // critical section here sem_post(&m); • Locks • Ensures that a critical section executes as if it were a single atomic instruction • Only have two states • Locked / Unlocked • Can be implemented with Binary Semaphore • Simply surround the critical section of interest with a sem_wait( ) / sem_post( ) pair • The initial value should be 1
Locks (Binary Semaphores) Semaphore -1 0 1 1 0 Run Context Switching! Context Switching! T0 Ready Wait T1 • An Example Scenario with Two Threads
Semaphores As Condition Variables • Condition Variable • Useful when a thread wants to halt waiting for a condition to become true • Waiting thread • Waiting for something to happen • Notifying thread • Making that something happen • Signaling that it has happened • Waking the waiting thread • Can be implemented with Private Semaphore • A waiting thread calls sem_wait( ) • A notifying thread calls sem_post( ) • The initial value should be 0
Semaphores As Condition Variables Semaphore 0 -1 0 Run Context Switching! Context Switching! Parent Ready Wait Child Z..z..z… • An Example Scenario: Parent waiting for Child • Case 1 • The parent creates the child but the child has not run yet
Semaphores As Condition Variables Semaphore 1 0 0 Run Context Switching! Context Switching! Parent Ready Wait Child • An Example Scenario: Parent waiting for Child • Case 2 • The child runs to completion before the parent gets a chance to call sem_wait( )
Producer / Consumer (Bounded-Buffer) Problem sem_t empty, full, mutex; int buffer[MAX]; int fill,use = 0; void put(int value) { buffer[fill] = value; fill = (fill + 1) % MAX; } int get() { int tmp = buffer[use]; use = (use + 1) % MAX; return tmp; } • Producer / Consumer Problem • Conditions • Producers MUST wait until a buffer is NOT FULL to place data in a buffer • ConsumersMUST wait until a buffer is NOT EMPTY to grab data items out of the buffer • Requirements • Two Semaphores(Empty / Full) • A lock for mutual exclusion • Put( ) and Get( ) routines
Producer / Consumer (Bounded-Buffer) Problem void *producer(void *arg) { inti; for (i = 0; i < loops; i++) { sem_wait(&empty); put(i); sem_post(&full); } } Empty Full Buffer X 1 0 -1 0 0 0 -1 Run void *consumer(void *arg) { inti, tmp = 0; while (tmp != -1) { sem_wait(&full); tmp = get(); sem_post(&empty); printf(“%d\n”, tmp); } } Context Switching! Context Switching! Consumer Ready Wait Producer • An Example Scenario • Two threads on a single CPU & Buffer size = 1
Producer / Consumer (Bounded-Buffer) Problem void *producer(void *arg) { inti; for (i = 0; i < loops; i++) { sem_wait(&empty); put(i); sem_post(&full); } } Buffer Empty Full 2 0 0 1 X1 X2 Overwritten!! Producer 2 Producer 1 void *producer(void *arg) { inti; for (i = 0; i < loops; i++) { sem_wait(&empty); put(i); sem_post(&full); } } Consumer 2 Consumer 1 • An Example Scenario • 2 producers and 2 consumers & Buffer size = 2 • Race Condition
Producer / Consumer (Bounded-Buffer) Problem void *producer(void *arg) { inti; for (i = 0; i < loops; i++) { sem_wait(&empty); sem_wait(&mutex); put(i); sem_post(&mutex); sem_post(&full); } } void *consumer(void *arg) { inti, tmp = 0; while (tmp != -1) { sem_wait(&full); sem_wait(&mutex); tmp = get(); sem_post(&mutex); sem_post(&empty); printf(“%d\n”, tmp); } } • An Example Scenario • 2 producers and 2 consumers & Buffer size = 2 • A Solution: Adding Mutual Exclusion to make put( ) and get( ) operations atomic !
Reader-Writer Locks typedef struct _rwlock_t { sem_t lock; // initial value is 1 sem_t writelock; // initial value is 1 int readers; // initial value is 0 } • Reader-Writer Locks • Functionality • Allows concurrent access for read-only operations while write operations require exclusive access • Requirements • A lock for writelock • A lock for mutual exclusion • A variable for counting the # of readers in critical section
Reader-Writer Locks Waiting… Waiting… void rwlock_acquire_readlock(rwlock_t *rw) { sem_wait(&rw->lock); rw->readers++; if (rw->readrers == 1) sem_wait(&rw->writelock); sem_post(&rw->lock); } void rwlock_release_readlock(rwlock_t *rw) { sem_wait(&rw->lock); rw->readers--; if (rw->readers == 0) sem_post(&rw->writelock); sem_post(&rw->lock); } void rwlock_acquire_writelock(rwlock_t *rw) { sem_wait(&rw->writelock); } void rwlock_release_writelock(rwlock_t *rw) { sem_post(&rw->writelock); } Reader 0 Reader 1 Writer 0 Reader 2 Reader 3 Waiting… Waiting… • An Example Scenario • A Writer got the lock first
Reader-Writer Locks Waiting… void rwlock_acquire_readlock(rwlock_t *rw) { sem_wait(&rw->lock); rw->readers++; if (rw->readrers == 1) sem_wait(&rw->writelock); sem_post(&rw->lock); } void rwlock_release_readlock(rwlock_t *rw) { sem_wait(&rw->lock); rw->readers--; if (rw->readers == 0) sem_post(&rw->writelock); sem_post(&rw->lock); } void rwlock_acquire_writelock(rwlock_t *rw) { sem_wait(&rw->writelock); } void rwlock_release_writelock(rwlock_t *rw) { sem_post(&rw->writelock); } Reader 0 Reader 1 Writer 0 Reader 2 Reader 3 • An Example Scenario • One of the Readers got the lock first
Reader-Writer Locks • Issues • Fairness • Easy for readers to starve writers (Writer starvation) • “Blocking new readers when a writer is blocker” can be a simple solution • Performance • Scale no better than having a single lock unless the lock hold time is long • Memory contention • Use of single word of memory to store the # of readers
Dining Philosopher’s Problem P1 P2 Fork 2 P0 Fork 1 Fork 3 Fork 0 P3 Fork 4 P4 • Dining Philosopher’s Problem • Rule • There are five philosophers and five forks • Each philosopher must alternately think and eat • A philosopher can only eat when he/she has both left and right forks • Each fork can be held by only one philosopher • After finishing eating, he/she needs to put down both forks • Problem • The contention for these forks • Synchronization problems • Goal • To design a protocol with • No deadlock • No starvation • High concurrency
Dining Philosopher’s Problem Waiting… int left(int p) { return p; } int right(int p) { return (p + 1) % 5; } void getforks() { sem_wait(forks[left(p)]); sem_wait(forks[right(p)]); } void putforks() { sem_post(forks[left(p)]); sem_post(forks[right(p)]); } P1 Waiting… P2 Fork 2 Waiting… Fork 1 P0 Fork 3 Deadlock!! Fork 0 Fork 4 P3 P4 Waiting… Waiting… • Naïve Approach • Every philosopher tries to pick left fork first
Dining Philosopher’s Problem Waiting… int left(int p) { return p; } int right(int p) { return (p + 1) % 5; } void getforks() { sem_wait(forks[left(p)]); sem_wait(forks[right(p)]); } void putforks() { sem_post(forks[left(p)]); sem_post(forks[right(p)]); } P1 Waiting… P2 Fork 2 Waiting… Fork 1 P0 Fork 3 Fork 0 Fork 4 P3 P4 Waiting… Waiting… • A Solution: Breaking the Dependency • Philosopher 4 tries to pick right fork first
How To Implement Semaphores typedef struct __Zem_t { int value; pthread_cond_t cond; pthread_mutex_t lock; } Zem_t; void Zem_init(Zem_t *s, int value) { s->value = value; Cond_init(&s->cond); Mutex_init(&s->lock); } void Zem_wait(Zem_t *s) { Mutex_lock(&s->lock); while (s->value <= 0) Cond_wait(&s->cond, &s->lock); s->value--; Mutex_unlcok(&s->lock); } void Zem_post(Zem_t *s) { Mutex_lock(*&s->lock); s->value++; Cond_signal(&s->cond); Mutex_unlock(&s->lcok); } • Zemaphores • Our own version of semaphores • Do not maintain the # of waiting threads • Can be implemented with Locks and CVs
Summary • Semaphores • Powerful and flexible primitives for writing concurrent programs • Surprisingly versatile usages • Locks • Condition Variables • Producer/Consumer (Bounded-Buffer) Problem • Reader-Writer Problem • Dining Philosopher’s Problem • Can be easily implemented with Locks and CVs