Table of Contents

CS111 Lecture 8 Scribe Notes

Date: April 27th, 2006

Synchronization (continue)

Atomic Instructions:

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?

  1. l unlocked:
    1. l = 1
    2. after test_and-set: l = 0
  2. l locked:
    1. l = 1
    2. test_and-set: take 1 out and then replace by 1 --> l = 1

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:

BLOCKING MUTEX

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);
}

Basic Synchronization Objects:

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.