Date: April 27th, 2006
LOCK OR MUTEX
VERSION 1: Spin Lock
test_and set(int *addr, int val)
{
int old = *addr;
*addr = val;
retun old;
}
typedef int mutex-t;
void lock (mutex_t *l)
{
while( test_and_set(l,1) == 1 )
/* DO NOTHING */ //--> spinlock
}
void unlock (mutex_t *l)
{
*l = 0;
}
Why does lock work?
Note: No race condition because these are atomic constructions
Disadvantage: This techinique is expensive because it continously changes the value of l
VERSION 2:
compare_and_swap( int *addr, int old, int new)
{
if( *addr == old)
{
*addr = new;
return 1;
}
else
return 0;
}
void lock( mutex_t l)
{
while( compare_and_swap(l,0,1) == 0 )
/* ... */
}
Advantage: don't have to change the value of l contiously
VERSION 3: Lock_free Deposit
...
int balance; //global value
...
if( cmd == deposit)
{
lock(&userlock);
balance += amount; //this addition need to be done atomically
unlock(&userlock);
}
Atomic addition:
<balacne += amount;>
while(1)
{
int b = balance;
if ( compare_and_swap(&balcane, b , b+amount) == 1)
break;
}
Example:
balance b ------- ---- $10 $10 //balacne start with $10 --> b is set to $10 $5 $10 //withdraw $5, -$5 from balance --> baclance = $5 compare_and_swap(&bal, 10, 15) will not success because bacle is not $10 now &5 $5 // b now is updated to $5 compare_and_swap(&bal, 5, 10) success!! --> balance = $10
Note: This technique works without lock
Conclusion on Spin Lock:
So far, we have only looked at spinlocks. We saw in the examples so far that when a lock function can't acquire a lock because someone else has it, it just goes into a while loop and continuously tries again. What's wrong with this? The spinlock uses up the rest of the thread/process's alotted time doing nothing, basically wasting resources that could be given to other jobs.
So how do we solve this? Use blocking of course!
With a blocking mutex, a process/thread blocks until there's a chance that it might get the lock. Let's compare spinlocks and blocking locks. (From here on I'll just refer to processes/threads as threads, since all processes contain at least one thread.)
Implementation of the blocking lock (1st try):
void lock(mutex_t *l)
{
while (test_and_set(l,1) == 1) //while someone else has the lock
sys_block(l); //block
}
void unlock(mutex_t *l)
{
*l = 0; //release lock
sys_unblock(&l); //wake up all threads waiting for the lock
}
What's wrong with this?
Implementation of the blocking lock (2nd try):
void lock(mutex_t *l)
{
while (test_and_set(l,1) == 1) //while someone else has the lock
sys_block(l); //block
}
void unlock(mutex_t *l)
{
*l = 0; //release lock
sys_unblock(&l); //wake up thread at start of wait queue
}
What else is wrong?
Implementation of the blocking lock (3rd try):
System calls:
sys_lock(mutex *l)
{
turn off interrupts
if (test_and_set(l, 1) == 1)
{
add thread to wait queue
turn on interrupts
return 0 and block
}
else
return 1
}
sys_unlock(mutex *l)
{
*l = 0;
wake up head of queue
}
User level implementation:
lock(mutex_t *l)
{
while (sys_lock(l) == 0);
}
unlock(mutex_t *l)
{
sys_unlock(l);
}
Yes...There's still something wrong!
Implementation of the blocking lock (Final try):
System calls:
sys_lock(mutex *l)
{
turn off interrupts
if (test_and_set(l, 1) == 1)
{
add thread to wait queue
turn on interrupts
block
when woken up, grab the lock and return 1
}
else
return 1
}
sys_unlock(mutex *l)
{
*l = 0;
wake up head of queue
and run it
}
User level implementation:
lock(mutex_t *l)
{
while (sys_lock(l) == 0);
}
unlock(mutex_t *l)
{
sys_unlock(l);
}
Semaphore (Invented by Edsder Dijkstra 1968): Semaphore is an OS abstract data type, and it will allow one process to control (lock) the shared resource while the other processes wait for the resource. Semaphore has two methods, V(s) and P(s). The V(s) operation unblocks a process by indivisibly signaling a blocked process to resume its operations. The P(s) operation indivisibly tests an integer variable and blocks the calling process if variable is not positive.
typedef int semaphore_t;
/* V operation is abbreviation for the Dutch word verhogen, meaning "to increment". */
V(semaphore_t s) // s is a nonnegative integer changed and tested only by V(s) and P(s) routines
{
s++ ; // an atomic operation
}
/* P operation is abbreviation for the Dutch word proberen, meaning "to test". */
P(semaphore_t s)
{
while(s == 0) // an atomic operation
block();
s--; // decrement is an atomic operation
}
Semphore mutex: lock(semaphore_t s) is equivalent to P(s), and unlock(semaphore_t s) is equivalent to V(s).
Monitors : A monitor is abstract data type for which only one process/thread may be executing any of its member procedures at any given time. Monitors are another attempt to provide better tools for solving synchronization problems. Every synchronization problem that can be solved with monitors can also be solved with semaphores. In monitors, a method locks mutex on entry and unlocks it on exit.
What happens when method is recursive?
monitor_mutex; //global variable
int fact(int f)
{
lock(&monitor_mutex) ; beginning of the critical section
int return_value;
if (f == 0) return_value = 1;
else return_value = f * fact(f-1); // Here it will block itself when the recursive call is made
unlock(& monitor_mutex) ; end of the critical section
return return_value;
}
In recursive method, the method blocks itself when the recursive call is made. To solve this problem we can use recursive mutex. Recursive mutex allows thread to relock the mutex. It is necessary to keep count of number of time the thread is locked.
void lock(mutex_t m)
{
per_thread_variable lock_count[m]; // for each recursive call
lock_count[m]++;
if (lock_count[m] == 1) sys_lock(m); // lock it very first time
}
void unlock(mutex_t m)
{
lock_count[m]--;
if(lock-count[m] == 0) sys_unlock(m);
}
Read/Write locks: Locking is used to ensure that some object doesn't change while it is in use. For example, if one thread is reading a file then it might lock the file to make sure that no other thread could change it. However, locking the file for only one thread’s read operation seems a little overprotective, since thread is not modifying the file. A read/write lock can be used to solve this problem. It allows any number of readers in a critical section or exactly one writer in the critical section, or no one. It has following four operations. Lock_read() and unock_read() operations acquire the lock for reading the object, while the lock_write() and unlock_write() operations acquire it for writing. However, multiple processes can be in a "read" critical section simultaneously. That is, "read" critical sections are non-exclusive. But "write" critical sections are exclusive both with each other, and with "read" critical sections.
typedef struct rwl
{
mutex_t m;
int rcount; // for number of readers
} rwl_t;
void lock_read(rwl_t *l)
{
Sys_lock(&l->m);
l->rcount++; // an atomic operation
sys_unlock(&l->m);
}
void unlock_read(rwl_t *l)
{
sys_lock(&l->m);
l->rcount--; // an atomic operation
sys_unlock(&l->m);
}
void lock_write(rwl_t *l)
{
while (1)
{
sys_lock(&l->m);
if (l->rcount == 0) // no other readers or writer
return;
sys_unlock(&l->m); // otherwise, try again
}
}
void unlock_write(rwl_t *l)
{
sys_unlock(&l->m);
}
Here the while loop is polling, we can increase the performance by using semaphore to do locking.