Lecture 009

Conditional Variable

The following code is correct, but the thread grabs the lock almost all the time, which is unfair:

right_time = false;
while (!right_time) {
  mutex_lock(&lock);
  // condition here happens once per year
  if (condition) {
    right_time = true;
  } else {
    mutex_unlock(&lock);
    // we can't add sleep() or yeield() here
    // because we can't predict exactly how long to sleep
  }
}
// do something at right time
mutex_unlock(&lock);

Condition Variable: We solve above issue by letting other thread tell us when to start running

cond_wait(cvar, world_lock) {
  lock(cvar->lock);
  enq(cvar->queue, my_thread_id());
  unlock(world_lock);
  ATOMICALLY {
    // to implement "ATOMICALLY":
    // we could disable interrupts
    // we could rely on OS (bad)
    // we could have a better thread-block interface (best)

    unlock(cvar->lock);
    thread_block(); // kernel please sleep me
    // we hope someone grab the cvar->lock and wake me up (not before I sleep)
  }
  lock(world_lock);
}

mutex_lock(&lock);
while (cvar->p = get_time_to_wait()) {
  cond_wait(cvar, &lock);
}
// do something at right time
mutex_unlock(&lock);

The other thread does

cond_broadcast(time_to_wait);

cond_wait() and cond_signal() can be called at the same time.

Conditional Variable Correctness:

Semaphore

If semaphore is only allows 0 and 1, then it's a mutex lock:

semaphore m = 1; // number of available critical sections
do {
  P(m); // wait
  // critical section
  V(m); // signal
  // non-critical section
} while (true);

There are all kinds of semaphore:

The implementation of wait() is similar to cond_wait().

wait(semaphore s) {
  lock(s->lock);
  --s->count;
  if (s->count < 0) {
    enq(s->queue, my_thread_id());
    ATOMICALLY {
      unlock(s->lock);
      thread_block();
    }
  } else {
    unlock(s->lock);
  }
}
signal(semaphore s) {
  lock(s->lock);
  ++s->count;
  if (s->count <= 0) {
    // wake up one thread
    int tid = deq(s->queue);
    thread_unblock(tid);
  }
  unlock(s->lock);
}

lock() and unlock() can be implemented using mutex or OS-assisted atomic de-scheduling / awakening.

Monitor

Some compiler adds synchronization code. To achieve: threads running in any procedure blocks all thread entires.

So imagine { and } are like lock() and unlock(), but they are invisible to programmers.

For waiting, programmers do this: (and is implemented via conditional variable)

// say someone tries to withdraw money before deposit, we want withdraw to wait
// deposit() will call cond_broadcast() when done
// This code is actually wrong
void stubbornly_cash_check(acc a, check c) {
  while(account[a].balance < check.val) {
    cond_wait(account[a].activity);
  }
  account[a].bal -= check.val;
}

What if you have signal() inside monitor block {}? No. We see signal() as a special kind of return. So signal() must be the last statement in the monitor block. (otherwise we have to deal with the case that the thread is woken up but the monitor is still locked)

More about async safety

Table of Content