Condition Variables

You work in a bar, pouring pints for the locals. One of your regulars comes in; he’s looking pretty grumpy today. “Whiskey” he snaps. You put down a glass and pour. You finish pouring and he necks back the drink. “Again”, he snaps. Again, you pour and as soon as you finish he necks it. This repeats two or three more times before the grumpy man slams down the money for his tab and leaves. Congratulations, you have just taken part in a “Producer/Consumer” exchange.

The “Producer/Consumer” (P/C) is one of the most well known and useful design patterns. It has a plethora of uses and, yet, it’s premise is very very simple. You have a “Producer”; an entity that provides something, and you have a “Consumer”; and entity that uses that resource. What makes the P/C pattern special is that access to the resource is mutually exclusive. When the Producer is producing the resource the Consumer has to wait. Just like, when you are pouring the drink the man has to wait. Likewise, when the Consumer is consuming the resource the Producer has to wait; try filling the man’s glass whilst he drinks if it’s not clear why this is so!

This article isn’t about the P/C design pattern, it’s about one of the fundamental thread synchronization objects used to successfully implement it. This article is about a simple but powerful entity called the “Condition Variable” (CV), so named because it is a variable that is shared between threads and used to allow one (or both) to notify the other of a certain condition. Never let it be said that programmers aren’t pragmatic when it comes to naming the tools of their trade!

The basic way a CV works is that it allows thread A to wait until such time that thread B signals that it can move on. You can sort of think of a CV as a smarter mutex, where as the thread in question can explicitly wait for another thread to tell it when it can go. This differs from a standard mutex. You can think of a standard mutex as just a gate, that is either locked or not. If it’s locked you wait and if it’s not you proceed.

A CV is more like a gate with a red light. When you arrive at the gate you press a button and the red light comes on. You then wait until the red light goes back out, indicating the person on the other side of the gate has acknowledged you and is now ready for you to enter. The door may or may not be locked… it doesn’t matter since you don’t enter until the light is gone from on to off.

It will probably not surprise you, then, to find out that the CV is actually implemented with the help of a mutex (it’s actually implemented indirectly via the unique_lock object, but this is just to ensure any locks owned are automatically released in the face of an exception). To use a CV thread A will apply a lock to a mutex and then pass the lock into the CV’s wait method. The mutex, up until this point, will be owned by thread A.

Once the wait method is called ownership of the mutex is released. Meanwhile, thread B will be waiting for ownership of the mutex. Once thread A is waiting, thread B is released. It does what it must (pour the drink!?) and then it calls the notify method.

Once the notify is called thread A is released and can continue on it’s merry way. At this point, both threads are now released. In more complex situations it may be necessary to have B then wait on A and A then wait on B and so on, but when this happens it’s just a repeat of the initial basic steps discussed (although when B wants to wait on A the steps are, of course, swapped so that B starts first).

This is one of those code-flows that is harder to explain than it is to write, so rather than trying to explain things in even more detail and getting everyone (including me) confused, let’s have a quick look at a simple example.

#include
#include
#include
#include 

using namespace std;

int main()
{
   mutex m;
   condition_variable cv;

   // THREAD PROC LAMBDA >>>
   auto tp = [&m, &cv]
   {
      // notify
      cout << "1" << endl;
      cv.notify_one();

      // wait
      {
         unique_lock l(m);
         cv.wait(l);
      }

      // notify
      cout << "3" << endl;
      cv.notify_one();

      // wait
      {
         unique_lock l(m);
         cv.wait(l);
      }

      // notify
      cout << "5" << endl;
      cv.notify_one();
   };
   // THREAD PROC LAMBDA <<<

   auto t = thread(tp);

   // wait
   {
      unique_lock l(m);
      cv.wait(l);
   }

   // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   // notify
   cout << "2" << endl;
   cv.notify_one();

   // wait
   {
      unique_lock l(m);
      cv.wait(l);
   }

   // notify
   cout << "4" << endl;
   cv.notify_one();

   // wait
   {
      unique_lock l(m);
      cv.wait(l);
   }

   t.join();

   cout << "6" << endl;
}

Notice how we have a very simple pattern being repeated?

  • Thread A: get scoped mutex lock
  • Thread A: wait on CV using mutex?
  • Thread B: do stuff
  • Thread B: notify
  • Thread B: get scoped mutex lock
  • Thread B: wait on CV using mutex
  • Thread A: do stuff
  • Thread A: notify
  • Thread A: get scoped mutex lock
  • Thread A: wait on CV using mutex
  • Thread B: do stuff
  • Thread B: notify
  • Thread B: get scoped mutex lock
  • Thread B: wait on CV using mutex
  • Thread A: do stuff
  • Thread A: notify
  • Thread A: get scoped mutex lock
  • Thread A: wait on CV using mutex
  • Thread B: do stuff
  • Thread B: notify
  • Thread B: thread exists
  • Thread A: wait for thread B to join

To summerise

  • For thread A to wait on thread be it needs to obtain a scoped unique_lock, and then use that with the CV’s wait method.
  • For thread B to tell thread A when it’s read it calls notify_one
  • For thread B to wait on thread be it needs to obtain a scoped unique_lock, and then use that with the CV’s wait method.
  • For thread A to tell thread A when it’s read it calls notify_one

That’s it, it’s a simple as that. Using this very simple pattern of locking, waiting and notifying two threads can dance around each other in a sort of coroutine tango.

But wait, I can hear you screaming… what does this have to do with the PC pattern? Well, here’s some pseudo code to clear that up.

drunk (consumer):
do
   wait for bartender to pour
   get glass
   drink whiskey
   put glass
repeat until passed out or no more whiskey

bartender (producer):
do
   get glass
   pour whiskey
   put glass
   wait for drunk to drink
repeat until drunk has passed out or no more whiskey

Whilst the CV is useful for many reasons when it comes to thread synchronization, the P/C pattern is by far the most obvious place for it to be used. It’s hard to see how this simple but incredibly useful pattern couple be implemented (so easily) without this flexible and effective thread primitive.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.