by: | Burt Rosenberg |
at: | university of miami |
for: | CSC421: Computer Operating Systems |
A lock L is built from test-and-set by having a memory local L.val and initializing it to 0. Then
L.test-and-set(v)Will set L.val=v and return the value of L.val just before this update.
Spin-locking is a simple but inefficient way to claim a lock: to repeatedly test-and-set a location until the return value indicates that the lock was unclaimed on call, but the test-and-set assures that the lock is claimed now.
It is safe in that at most one thread will ever have claimed the lock, and it is live in that if the lock is unclaimed on call, it will be claimed. It is not fair nor is it efficient. These defects are acknowledged in that the use of spin-locks are only for the lowest level of contention resolution.
TEST-AND-SET and SPIN-LOCK On L.test-and-set(v): atomic { t = L.val L.val = v return t } LOCK.init: alloc L L.val = 0 return L LOCK.lock: return L.test-and-set(1) LOCK.release: # PRE: lock held L.val = 0 LOCK.spin-lock: while L.lock: pass # POST: lock held return
THE PV-SEMAPHORE On S.P: decrement S.counter if S.counter is negative, place the calling thread on S.queue, and sleep the thread. On S.V: increment S.counter. if S.queue is not empty, remove any thread from S.queue and resume the thread.
Here is why this is important. Without synchronization, starting with S.counter==1, if both threads call S.P simultaneously, both threads see S.counter==1, both threads set S.counter to 0, and both threads continue.
This is incorrect behavior.
If two threads call S.P simultaneously, one P operation must occur fully before the other P operation. And with this contraint, exactly one of the two threads sees S.counter==1, sets it to 0 and continues. The other thread sees S.counter==0, sets it to -1, and sleeps. That is the correct behavior.
If the P cannot claim the semaphore, that the sleep of the thread will be handled by the scheduler. The thread will be placed into the wait state and the lock released.
The lock will be held only for the briefest time possible, so that any spin-lock, while inefficient, will be brief.
struct PV_Semaphore { int lock ; int count ; struct WaitList * waitlist ; } extern thread_t current_thread ; void P(struct PV_Semaphore * s) { while (test_and_set(&s->lock,1)==1) ; s->count -- ; if (s->count<0) { s->waitlist.add(current_thread) ; set_state(current_thread, WAITING) ; s->lock = 0 ; current_thread.yield() ; return ; } s->lock = 0 ; return ; } void V(struct PV_Semaphore * s) { while (test_and_set(&s->lock,1)==1) ; s->count ++ ; if (!s->waitlist.is_empty()) { t = s->waitlist.remove() ; // remove one at random set_state(t, READY) ; } s->lock = 0 ; return ; }
=== locked ~~~ wait/sleep P V | | A: ----+=============+--------------- | | B: --------+~~~~~~~~~+======+-------- | | P V
Said monitor as the following API,
// solution to monitor project // burt rosenberg, oct 2010 // updated: oct 2011 // REMEMBER THIS IS PSEUDO-CODE! semaphore lock_sema semaphore wait_sema integer wait_count init: lock_sema unlocked (1) wait_sema locked (0) wait_count = 0 enter: assume: not holding lock_sema down on lock_sema leave: assume: holding lock_sema up on lock_sema wait: assume: holding lock_sema // increment must be atomic wait_count++ up on lock_sema // no critical race because wait_count // registers interest in waiting down on wait_sema // contend to reenter down on lock_sema notify: assume: holding lock_sema // are "if" and decrement SMP safe? yes, lock_sema protects wait_count. if wait_count>0 wait_count-- up on wait_sema notify_all: assume: holding lock_sema while wait_count>0 wait_count-- up on wait_sema
It is not possible to query the semaphore whether there is a thread waiting, and notifying conditional on the result, because of a race condition in the wait implementation between the up on lock_sema and the down on wait_seam.
The wait_count is always updated when hold the monitor lock so it has the correct number of waiting threads. If wait is called in thread T1 is on the way to waiting when thread T2 enters notify, the wait_sema will be correctly up'ed, and T1 will continue with it reaches the semaphore and downs it.
There is one such lock for each object instance. Re-entrantcy (re-acquiring a lock already held) and leave are handled correctly as part of the language.
author: burton rosenberg
created: 14 oct 2024
update: 29 sep 2025