Skip to main content
Engineering LibreTexts

10.4: Condition variables

  • Page ID
    40637
  • A condition variable is a data structure associated with a condition; it allows threads to block until the condition becomes true. For example, thread_pop might want check whether the queue is empty and, if so, wait for a condition like “queue not empty”.

    Similarly, thread_push might want to check whether the queue is full and, if so, block until it is not full.

    I’ll handle the first condition here, and you will have a chance to handle the second condition as an exercise.

    First we add a condition variable to the Queue structure:

    typedef struct {
        int *array;
        int length;
        int next_in;
        int next_out;
        Mutex *mutex;
        Cond *nonempty;   //-- new
    } Queue;
    

    And initialize it in make_queue:

    Queue *make_queue(int length)
    {
        Queue *queue = (Queue *) malloc(sizeof(Queue));
        queue->length = length;
        queue->array = (int *) malloc(length * sizeof(int));
        queue->next_in = 0;
        queue->next_out = 0;
        queue->mutex = make_mutex();
        queue->nonempty = make_cond();   //-- new
        return queue;
    }
    

    Now in queue_pop, if we find the queue empty, we don’t exit; instead we use the condition variable to block:

    int queue_pop(Queue *queue) {
      mutex_lock(queue->mutex);
      while (queue_empty(queue)) {
        cond_wait(queue->nonempty, queue->mutex);  //-- new
      }
      
      int item = queue->array[queue->next_out];
      queue->next_out = queue_incr(queue, queue->next_out);
      mutex_unlock(queue->mutex);
      cond_signal(queue->nonfull);   //-- new
      return item;
    }
    

    cond_wait is complicated, so let’s take it slow. The first argument is the condition variable; in this case, the condition we are waiting for is “queue not empty”. The second argument is the mutex that protects the queue.

    When the thread that locked the mutex calls cond_wait, it unlocks the mutex and then blocks. This is important. If cond_wait did not unlock the mutex before blocking, no other thread would be able to access the queue, no more items could be added, and the queue would always be empty.

    So while the consumer is blocked on nonempty, the producer can run. Let’s see what happens when the producer runs queue_push:

    void queue_push(Queue *queue, int item) {
        mutex_lock(queue->mutex);
        if (queue_full(queue)) {
            mutex_unlock(queue->mutex);
            perror_exit("queue is full");
        }
        queue->array[queue->next_in] = item;
        queue->next_in = queue_incr(queue, queue->next_in);
        mutex_unlock(queue->mutex);
        cond_signal(queue->nonempty);    //-- new
    }
    

    Just as before, queue_push locks the Mutex and checks whether the queue is full. Assuming it is not, queue_push adds a new element to the queue and then unlocks the Mutex.

    But before returning, it does one more thing: it “signals” the condition variable nonempty.

    Signalling a condition variable usually indicates that the condition is true. If there are no threads waiting on the condition variable, the signal has no effect.

    If there are threads waiting on the condition variable, one of them gets unblocked and resumes execution of cond_wait. But before the awakened thread can return from cond_wait, it has to wait for and lock the Mutex, again.

    Now go back to queue_pop and see what happens when the thread returns from cond_wait. It loops back to the top of the while loop and checks the condition again. I’ll explain why in just a second, but for now let’s assume that the condition is true; that is, the queue is not empty.

    When the consumer thread exits the while loop, we know two things: (1) the condition is true, so there is at least one item in the queue, and (2) the Mutex is locked, so it is safe to access the queue.

    After removing an item, queue_pop unlocks the mutex and returns.

    In the next section I’ll show you how my Cond code works, but first I want to answer two frequently-asked questions:

    • Why is cond_wait inside a while loop rather than an if statement; that is, why do we have to check the condition again after returning from cond_wait?

      The primary reason you have to re-check the condition is the possibility of an intercepted signal. Suppose Thread A is waiting on nonempty. Thread B adds an item to the queue and signals nonempty. Thread A wakes up an tries to lock the mutex, but before it gets the chance, Evil Thread C swoops in, locks the mutex, pops the item from the queue, and unlocks the mutex. Now the queue is empty again, but Thread A is not blocked any more. Thread A could lock the mutex and returns from cond_wait. If Thread A does not check the condition again, it would try to pop an element from an empty queue, and probably cause an error.

    • The other question that comes up when people learn about condition variables is “How does the condition variable know what condition it is associated with?”

      This question is understandable because there is no explicit connection between a Cond structure and the condition it relates to. The connection is implicit in the way it is used.

      Here’s one way to think of it: the condition associated with a Cond is the thing that is false when you call cond_wait and true when you call cond_signal.

    Because threads have to check the condition when they return from cond_wait, it is not strictly necessary to call cond_signal only when the condition is true. If you have reason to think the condition might be true, you could call cond_signal as a suggestion that now is a good time to check.

    • Was this article helpful?