You are expected to understand this. CS 111 Operating Systems Principles, Spring 2006
You are here: CS111: [[2006spring:notes:lec8]]
 
 
 

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:

  • Inefficient becuase implement use polling, which is busy waiting.
  • This problem can be solve by using Blocking Mutex.

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.)

  • Worst version: a spinlock does nothing for the rest of its alloted time
  • better version: a spinlock yields to other threads when it can't get the lock, but doesn't block. This way other threads can use its remaining processor time. However, since the thread doesn't block, the next time it's run the lock might still be unavailable. (weensyos1's original thread joining algorithm uses this method - see the original implementation of TRAP_SYS_JOIN in threados-kern.c)
  • blocking version: there's a wait queue associated with each lock. When a thread can't get the lock, it goes to sleep (blocks) and gets put on a wait queue. When another thread who has the lock releases it, the blocked thread will wake up.

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?

  • Problem: there's no bounded waiting time (no fairness) - since the unlocking code always wakes up all waiting threads, if a thread is always "behind" someone, it'll never get the lock!
  • Solution: set up a first in first out wait queue! This ensures that all threads have a chance to get the lock. The sys_block() system call adds calling thread to end of queue, and sys_unblock only wakes up a thread at the head of queue.

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?

  • Problem: What if a thread's time runs out between test_and_set and sys_block or between *l = 0 and sys_unblock? For example, if test_and_set returns 1, it means some other thread has the lock, and the thread prepares to block. But before it gets to sys_block(), the kernel decides to switch to the thread that has the lock. That thread decides to release the lock. When the kernel lets our doomed thread run again, it'll call sys_block() even though the lock has been freed! It may never run again unless some other thread decides to get the release the lock. A similar situation may happen for the unlocking code. A thread that has the lock decides to release it. It sets the lock to 0 and prepares to unblock the head of the queue. But before it can do that the kernel switches to another thread that just happens to want the lock. That thread would see the unlocked lock and grab it for itself. This is unfair because that thread just cutted in line!
  • These race conditions are known as the sleep-wakeup race.
  • Solution: We need these instructions to execute without interrupts (atomic). The only way to do this would be to change them to system calls and have the kernel handle them, since the kernel is never preempted.

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!

  • Problem: What if a thread has the lock and decides to release it. So it calls the unlock function, which calls the sys_unlock system call. The lock is set to 0, and wakes up the thread at the head of queue. But what if that woken up thread doesn't get scheduled right away? We only unblocked that thread, we didn't schedule it to run. Another thread that wants that same lock might get run before the thread that waited in line did. The thread that cut in line would get the lock instead of the thread that got woken up! We have the same problem of unfairness again.
  • Solution: instead of just waking up the thread, we also run it immediately.

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.

 
2006spring/notes/lec8.txt · Last modified: 2006/09/26 11:42 (external edit)
 
Recent changes RSS feed Driven by DokuWiki