Using Condition Variables
In addition to mutual exclusion, you can also synchronize threads using condition synchronization.
In condition synchronization, a thread is delayed (blocked) until the program state satisfies some predicate or condition. A key mechanism for implementing condition synchronization is the condition variable.
A condition variable is an efficient mechanism for waiting for and signaling changes in program state. The basic condition variable defines two operations:
• wait—Causes the calling thread to block until another thread calls the signal function.
• signal—Wakes up one waiting thread.
A condition variable also includes a signal-all, or broadcast operation that can be used to awaken all waiting threads, not just one.
Combining the Condition Variable with Mutual Exclusion
In this form of condition synchronization, the mutex is used to prohibit changes to the program state upon which the synchronization condition depends. A thread subjected to condition synchronization acquires the mutex, tests the condition, and if the condition is found to be false, enters the condition-variable wait. This wait operation temporarily unlocks the mutex so that other threads can access and update the program state.
A thread that is going to change the program state acquires the mutex, updates the state, and calls the condition-variable’s signal operation to wake-up a thread that is waiting. When the waiting thread is awakened, it reacquires the mutex in preparation for a reevaluation of the synchronization condition. If the condition is still found to be false, the thread again enters the wait. If the condition is found to be true, the thread has achieved synchronization and can now release the mutex and proceed.
In the Synchronization package, condition variable synchronization is included in the
RWCondition class. This class combines the mutex and condition-variable interface. An
RWCondition class instance does not possess its own mutex; you must supply a reference to an
RWMutexLock instance when you construct each condition instance. See
“The RWCondition Class” for more information about using this class.
Using the Condition Variable for Producer-Consumer Synchronization
Condition variables are ideal for implementing producer-consumer synchronization, a common form of condition synchronization used to mediate the transfer of information between threads. Under this form of synchronization, a consumer thread attempts to get information from a producer thread or threads. If that information is not immediately available, it waits for a producer thread to signal when the information becomes available.
Similarly, a producer thread attempts to send information to a consumer thread or threads. If no consumer thread is waiting for the information or if the information cannot be buffered, then the producer thread waits for a consumer thread to signal that it has received or is ready to receive information.
Example 31 uses the linked-list template
RWTValSlist, from the Essential Tools Module, and the Synchronization package classes
RWMutexLock and
RWCondition to illustrate how producer-consumer synchronization can be used to turn a simple queue into an elegant and simple mechanism for efficiently communicating between threads.
Example 31 – Using condition variables for producer-consumer synchronization
#include <rw/tvslist.h>
#include <rw/sync/RWMutexLock.h>
#include <rw/sync/RWCondition.h>
template <class T> class PCQueue {
private:
RWMutexLock mutex; // 1
RWCondition notEmpty; // 2
RWCondition notFull;
RWTValSlist<T> queue; // 3
size_t maxSize;
public:
PCQueue(size_t size=0) // 4
: maxSize(size), notEmpty(mutex), notFull(mutex) // 5
{
}
T read(void)
{
RWMutexLock::LockGuard guard(mutex); // 6
while(queue.isEmpty()) { // 7
notEmpty.wait(); // 8
}
T result = queue.removeFirst(); // 9
notFull.signal(); // 10
return result;
}
void write(T t)
{
RWMutexLock::LockGuard guard(mutex);
while(maxSize != 0 && queue.entries() <= maxSize) { // 11
notFull.wait(); // 12
}
queue.append(t); // 13
notEmpty.signal(); // 14
}
};
A full implementation of a producer-consumer queue would also have member functions for testing whether the queue was currently empty or full, thereby allowing threads to avoid blocking during a read or write operation. These functions and more have already been defined in an existing family of producer-consumer queue and stack classes in the Interthread Communication package. These classes are described in
“The Producer‑Consumer Classes.”