170 likes | 286 Vues
This text delves into synchronization mechanisms in computing, particularly focusing on lock contention, critical sections, and system calls. It addresses the overhead involved in protecting shared variables, explores the behavior of locks under contention, and evaluates different synchronization schemes. The concept of semaphores is introduced, explaining the 'P' (wait) and 'V' (signal) operations, and their implementations in bounded buffer scenarios. Various solutions for semaphore implementation are also discussed, highlighting the work of different researchers and the complexities involved in achieving efficient synchronization.
E N D
More Synchronization, Semaphores Vivek Pai / Kai Li Princeton University
Continuing on Synchronization So far, we’ve seen • “Spinning” on lock during entire critical section • Disabling interrupts for critical section (bad) • Queue associated with each lock & blocking • System calls for locking – possibly blocking Since system calls exist, is everything solved? Assume shared variable “count” Lock(&mutex); count++; Unlock(&mutex);
Cost of Protecting a Shared Variable • Making lock system call • Pushing parameter, sys call # onto stack • Generating trap/interrupt to enter kernel • System call in kernel • Jump to appropriate function in kernel • Verify process passed in valid pointer to mutex • Do locking operation, block process if needed • Actually change count – load/modify/store • System call again to release mutex
What is Lock Contention? • Competition for a lock • Uncontended = rarely in use by someone else • Contended = often used by someone else • Held = currently in use by someone • Question: what do these combinations do? • Spinning on low-contention lock • Spinning on high-contention lock • Blocking on low-contention lock • Blocking on high-contention lock
Things to Ponder • If critical section is just “count++;”, what is the overhead of the synchronization • Is there some other way of doing this? • What if you don’t know how long the critical section will be?
What If You Have the Following • Test-and-set – works at either user or kernel • System calls for block/unblock • Block takes some token and goes to sleep • Unblock “wakes up” a waiter on token
User-Level Acquire/Release using Block and Unblock • In what scenarios is this scheme appropriate? • Where should it not be used? Release(lock) { lock = 0; Unblock( lock ); } Acquire(lock) { while (!TAS(lock)) Block( lock ); }
Semaphores (Dijkstra, 1965) • Up or “V” • Atomic • Increment semaphore by 1 wake up a waiting P if any • Down or “P” • Atomic • Wait for semaphore to become positive and then decrement by 1 P(s) { if (--s < 0) Block(s); } V(s) { if (++s <= 0) Unblock(s); }
Bounded Buffer (Consumer-Producer) • Example: • grep vivek access.log | more Producer Consumer
Bounded Buffer w/ Semaphores mutex = 1 emptyCount = N; fullCount = 0; producer() { while (1) { produce an item P(emptyCount); P(mutex); put the item in buffer V(mutex); V(fullCount); } } consumer() { while (1) { P(fullCount); P(mutex); take an item from buffer V(mutex); V(emptyCount); consume the item } }
Implementing General Semaphores • Need a mutex for each semaphore • Block and Unblock need to release mutex after entering their critical section V(s) { Acquire(s.mutex); if (++s.value <= 0) Unblock(s); else Release(s.mutex) } P(s) { Acquire(s.mutex); if (--s.value < 0) Block(s); else Release(s.mutex) }
Implement General Semaphores with Acquire/Release • Kotulski (1988) • Two processes call P(s) (when s.value is 0) and preempted after Release(s.mutex) • Two other processes call V(s) P(s) { Acquire(s.mutex); if (--s.value < 0) { Release(s.mutex); Acquire(s.delay); } else Release(s.mutex); } V(s) { Acquire(s.mutex); if (++s.value <= 0) Release(s.delay); Release(s.mutex); }
Hemmendinger’s Solution (1988) • The idea is not to release s.mutex and turn it over individually to the waiting process • P and V are executing in lockstep P(s) { Acquire(s.mutex); if (--s.value < 0) { Release(s.mutex); Acquire(s.delay); } Release(s.mutex); } V(s) { Acquire(s.mutex); if (++s.value <= 0) Release(s.delay); else Release(s.mutex); }
Kearns’s Solution (1988) P(s) { Acquire(s.mutex); if (--s.value < 0) { Release(s.mutex); Acquire(s.delay); Acquire(s.mutex); if (--s.wakecount > 0) Release(s.delay); } Release(s.mutex); } V(s) { Acquire(s.mutex); if (++s.value <= 0) { s.wakecount++; Release(s.delay); } Release(s.mutex); } Two Release( s.delay) calls are also possible
Hemmendinger’s Correction (1989) P(s) { Acquire(s.mutex); if (--s.value < 0) { Release(s.mutex); Acquire(s.delay); Acquire(s.mutex); if (--s.wakecount > 0) Release(s.delay); } Release(s.mutex); } V(s) { Acquire(s.mutex); if (++s.value <= 0) { s.wakecount++; if (s.wakecount == 1) Release(s.delay); } Release(s.mutex); } Correct but a complex solution
Hsieh’s Solution (1989) P(s) { Acquire(s.delay); Acquire(s.mutex); if (--s.value > 0) Release(s.delay); Release(s.mutex); } V(s) { Acquire(s.mutex); if (++s.value == 1) Release(s.delay); Release(s.mutex); } • Use Acquire(s.delay) to block processes • Correct but still a constrained implementation