Operating Systems Fall 2014 Semaphores, Condition Variables, and Monitors Myungjin Lee myungjin.lee@ed.ac.uk 1
Semaphores • Semaphore = a synchronization primitive – higher level of abstraction than locks – invented by Dijkstra in 1968, as part of the THE operating system • A semaphore is: – a variable that is manipulated through two operations, P and V (Dutch for “wait” and “signal”) • P(sem) (wait) – block until sem > 0, then subtract 1 from sem and proceed • V(sem) (signal) – add 1 to sem • Do these operations atomically 2
Blocking in semaphores • Each semaphore has an associated queue of threads – when P (sem) is called by a thread, • if sem was “available” (>0), decrement sem and let thread continue • if sem was “unavailable” (0), place thread on associated queue; run some other thread – when V (sem) is called by a thread • if thread(s) are waiting on the associated queue, unblock one – place it on the ready queue – might as well let the “V-ing” thread continue execution • otherwise (when no threads are waiting on the sem), increment sem – the signal is “remembered” for next time P(sem) is called 3
Two types of semaphores • Binary semaphore (aka mutex semaphore) – sem is initialized to 1 – guarantees mutually exclusive access to resource (e.g., a critical section of code) – only one thread/process allowed entry at a time – Logically equivalent to a lock with blocking rather than spinning • Counting semaphore – Allow up to N threads continue (we’ll see why in a bit …) – sem is initialized to N • N = number of units available – represents resources with many (identical) units available – allows threads to enter as long as more units are available 4
Binary semaphore usage • From the programmer’s perspective, P and V on a binary semaphore are just like Acquire and Release on a lock P(sem) . . . do whatever stuff requires mutual exclusion; could conceivably be a lot of code . . . V(sem) – same lack of programming language support for correct usage • Important differences in the underlying implementation, however 5
Example: Bounded buffer problem • AKA “producer/consumer” problem – there is a circular buffer in memory with N entries (slots) – producer threads insert entries into it (one at a time) – consumer threads remove entries from it (one at a time) • Threads are concurrent – so, we must use synchronization constructs to control access to shared variables describing buffer state tail head 6
Bounded buffer using semaphores (both binary and counting) var mutex: semaphore = 1 ; mutual exclusion to shared data empty: semaphore = n ; count of empty slots (all empty to start) full: semaphore = 0 ; count of full slots (none full to start) producer: P(empty) ; block if no slots available P(mutex) ; get access to pointers Note: <add item to slot, adjust pointers> I have elided all the code V(mutex) ; done with pointers concerning which is the first V(full) ; note one more full slot full slot, which is the last full slot, etc. consumer: P(full) ; wait until there’s a full slot P(mutex) ; get access to pointers <remove item from slot, adjust pointers> V(mutex) ; done with pointers V(empty) ; note there’s an empty slot <use the item> 7
Example: Readers/Writers • Description: – A single object is shared among several threads/processes – Sometimes a thread just reads the object – Sometimes a thread updates (writes) the object – We can allow multiple readers at a time • why? – We can only allow one writer at a time • why? 8
Readers/Writers using semaphores var mutex: semaphore = 1 ; controls access to readcount wrt: semaphore = 1 ; control entry for a writer or first reader readcount: integer = 0 ; number of active readers writer: P(wrt) ; any writers or readers? <perform write operation> V(wrt) ; allow others reader: P(mutex) ; ensure exclusion readcount++ ; one more reader if readcount == 1 then P(wrt) ; if we’re the first, synch with writers V(mutex) <perform read operation> P(mutex) ; ensure exclusion readcount-- ; one fewer reader if readcount == 0 then V(wrt) ; no more readers, allow a writer V(mutex) 9
Readers/Writers notes • Notes: – the first reader blocks on P(wrt) if there is a writer • any other readers will then block on P(mutex) – if a waiting writer exists, the last reader to exit signals the waiting writer • can new readers get in while a writer is waiting? • so? – when writer exits, if there is both a reader and writer waiting, which one goes next? 10
Semaphores vs. Spinlocks • Threads that are blocked at the level of program logic (that is, by the semaphore P operation) are placed on queues, rather than busy-waiting • Busy-waiting may be used for the “real” mutual exclusion required to implement P and V – but these are very short critical sections – totally independent of program logic – and they are not implemented by the application programmer 11
Abstract implementation – P/wait(sem) • acquire “real” mutual exclusion – if sem is “available” (>0), decrement sem; release “real” mutual exclusion; let thread continue – otherwise, place thread on associated queue; release “real” mutual exclusion; run some other thread – V/signal(sem) • acquire “real” mutual exclusion – if thread(s) are waiting on the associated queue, unblock one (place it on the ready queue) – if no threads are on the queue, sem is incremented » the signal is “remembered” for next time P(sem) is called • release “real” mutual exclusion • [the “V-ing” thread continues execution, or may be preempted] 12
Pressing questions • How do you acquire “real” mutual exclusion? • Why is this any better than using a spinlock (test-and-set) or disabling interrupts (assuming you’re in the kernel) in lieu of a semaphore? • What if some bozo issues an extra V? • What if some bozo forgets to P before manipulating shared state? • Could locks be implemented in exactly the same way? That is, “software locks” that you acquire and release, where the underlying implementation involves moving descriptors to/from a wait queue? 13
Condition Variables • Basic operations – Wait() • Wait until some thread does a signal and release the associated lock, as an atomic operation – Signal() • If any threads are waiting, wake up one • Cannot proceed until lock re-acquired • Signal() is not remembered – A signal to a condition variable that has no threads waiting is a no- op • Qualitative use guideline – You wait() when you can’t proceed until some shared state changes – You signal() when shared state changes from “bad” to “good” 14
Bounded buffers with condition variables var mutex: lock ; mutual exclusion to shared data freeslot: condition ; there’s a free slot fullslot: condition ; there’s a full slot Note 1: Do you see why wait() must release the associated producer: lock? lock(mutex) ; get access to pointers if [no slots available] wait(freeslot); Note 2: <add item to slot, adjust pointers> How is the associated lock signal(fullslot); re-acquired? unlock(mutex) [Let’s think about the implementation of this inside the threads package] consumer: lock(mutex) ; get access to pointers if [no slots have data] wait(fullslot); <remove item from slot, adjust pointers> signal(freeslot); unlock(mutex); <use the item> 15
The possible bug • Depending on the implementation … – Between the time a thread is woken up by signal() and the time it re- acquires the lock, the condition it is waiting for may be false again • Waiting for a thread to put something in the buffer • A thread does, and signals • Now another thread comes along and consumes it • Then the “signalled” thread forges ahead … – Solution • Not – if [no slots available] wait(fullslot) • Instead – While [no slots available] wait(fullslot) – Could the scheduler also solve this problem? 16
The possible bug Y-axis is time Waiting Another Producer consumer T1 consumer T2 T3 Arrives at the Insert critical section Waiting signal an item Unlock mutex Wake up mutex is free Lock the mutex Consume an item Unlock the mutex Reacquire mutex Try to consume an item (but already consumed by T2 ) Unlock the mutex 17
Problems with semaphores, locks, and condition variables • They can be used to solve any of the traditional synchronization problems, but it’s easy to make mistakes – they are essentially shared global variables • can be accessed from anywhere (bad software engineering) – there is no connection between the synchronization variable and the data being controlled by it – No control over their use, no guarantee of proper usage • Condition variables: will there ever be a signal? • Semaphores: will there ever be a V()? • Locks: did you lock when necessary? Unlock at the right time? At all? • Thus, they are prone to bugs – We can reduce the chance of bugs by “stylizing” the use of synchronization – Language help is useful for this 18
Recommend
More recommend