Return to the lecture notes index

Lecture 5 (September 9, 2008)

Condition Variables - Modelling Events

Last class, we discussed sempahores. The semaphore gave us a way of modelling the usage of a resource, or a pool of interchangable resources, in a way that makes concurrent programming much easier.

Today we'll talk about the condition variables, a synchronization primitive that helps us model events rather than resources. To understand the difference between an event and a resource, consider a stadium of fans waiting for a team to score. When the team scores, all of the fans cheer. Now imagine a tree falling in a forrest -- with no one there to hear it. It doesn't make a sound later on as some hikers pass by. Instead, the sound is just missed -- it has no subsequent consequences.

Both the falling of the team and the team scoring are events. We see that, unlike resoures, sometimes a single event is of interest to many. And, when an event occurs, it is transient -- unlike a resource which becomes available until it is consumed.

Condition Variables - Operations

Condition variables support three operations:

It is very, very important to note that, unlike the P/wait operation upon a semaphore, the wait operation upon a condition variable always and immediately blocks. This makes using condtion variables slightly more nuianced than using semaphores. Unlike a semaphore, which can atomically check a condition and if appropraite block, the wait upon a condition variable isn't predicated -- it is guaranteed.

Because of this, when using condition variables, an additional mutex must be used to protect the critical sections of code that test the lock or change the locks state. We'll see how this all works momentarily.

Condition Variables - Typical Use

The following code illustrates a typical use of condition variables to acquire a resource. Notes that both the mutex mx and the condition variable cv are passed into the wait function.

If you examine the implementation of wait below, you will find that the wait function atomically releases the mutex and puts the thread to sleep. After the thread is signalled and wakes up, it reacquires the resource. This is to prevent a lost wake-up. This situation is discussed in the section describing the implementation of condition variables.

  spin_lock s;

  GetLock (condition cv, mutex mx)
  {
    mutex_acquire (mx);
    while (LOCKED)
      wait (c, mx);
    
    lock=LOCKED;
    mutex_release (mx);
  }


  ReleaseLock (condition cv, mutex mx)
  {
    mutex_acquire (mx);
      lock = UNLOCKED;
      signal (cv);
    mutex_release (mx);
  }
  

Condition Variables - Implementation

This is just one implementation of condition variables, others are possible.

Data Structure

The condition variable data structure contains a double-linked list to use as a queue. It also contains a semaphore to protect operations on this queue. This semaphore should be a spin-lock since it will only be held for very short periods of time.

  struct condition {
    proc next;  /* doubly linked list implementation of */
    proc prev;  /* queue for blocked threads */ 
    mutex mx; /*protects queue */
  };
  

wait()

The wait() operation adds a thread to the list and then puts it to sleep. The mutex that protects the critical section in the calling function is passed as a parameter to wait(). This allows wait to atomically release the mutex and put the process to sleep.

If this operation is not atomic and a context switch occurs after the release_mutex (mx) and before the thread goes to sleep, it is possible that a process will signal before the process goes to sleep. When the waiting() process is restored to execution, it will enter the sleep queue, but the message to wake it up will be forever gone.

  void wait (condition *cv, mutex *mx) 
  {
    mutex_acquire(&c->listLock);  /* protect the queue */
    enqueue (&c->next, &c->prev, thr_self()); /* enqueue */
    mutex_release (&c->listLock); /* we're done with the list */
  
    /* The suspend and release_mutex() operation should be atomic */
    release_mutex (mx));
    thr_suspend (self);  /* Sleep 'til someone wakes us */
  
    mutex_acquire (mx); /* Woke up -- our turn, get resource lock */
  
    return;
  }
  

signal()

The signal() operation gets the next thread from the queue and wakes it up. If the queue is empty, it does nothing.

  void signal (condition *c)
  {
    thread_id tid;

    mutex_acquire (c->listlock); /* protect the queue */
    tid = dequeue(&c->next, &c->prev);
    mutex_release (listLock);
  
    if (tid>0)
      thr_continue (tid);

    return;
  }
  

broadcast()

The broadcast operation wakes up every thread waiting for a particular resource. This generally makes sense only with sharable resources. Perhaps a writer just completed so all of the readers can be awakened.

  void broadcast (condition *c)
  {
    thread_id tid;

    mutex_acquire (c->listLock); /* protect the queue */
    while (&c->next) /* queue is not empty */
    {
      tid = dequeue(&c->next, &c->prev); /* wake one */
      thr_continue (tid); /* Make it runnable */
    }
    mutex_release (c->listLock); /* done with the queue */
  }