Condition variables are similar to monitors in that they allow a thread to enter a critical region, test a condition, and sleep inside the critical region while releasing its lock. Another thread then is free to enter the critical region, change the condition, and eventually signal the sleeping thread, which resumes at the point where it went to sleep and with the critical region once again locked.
Note that condition variables provide subset of the functionality of monitors, so a monitor can always be used instead of a condition variable. However, condition variables are smaller, which may be important if you are seriously constrained with respect to memory.
Condition variables are provided by the IceUtil::Cond class. Here is its interface:
class Cond : private noncopyable {
public:
Cond();
~Cond();
void signal();
void broadcast();
template<typename Lock>
void wait(const Lock& lock) const;
template<typename Lock>
bool timedWait(const Lock& lock,
const Time& timeout) const;
};
Using a condition variable is very similar to using a monitor. The main difference in the
Cond interface is that the
wait and
timedWait member functions are template functions, instead of the entire class being a template. The member functions behave as follows:
•
Do not call wait or
timedWait unless you hold the lock.
In contrast to monitors, which require you to call notify and
notifyAll with the lock held, condition variables permit you call
signal and
broadcast without holding the lock. Here is a code example that changes a condition and signals on a condition variable:
Mutex m;
Cond c;
// ...
{
Mutex::Lock sync(m);
// Change some condition other threads may be sleeping on...
c.signal();
// ...
} // m is unlocked here
{
Mutex::Lock sync(m);
while(!condition) {
c.wait(sync);
}
// Condition is now true, do
// some processing...
} // m is unlocked here
Again, this code is correct and will work as intended. However, consider what can happen once the first thread calls
signal. It is possible that the call to signal will cause an immediate context switch to the waiting thread. But, even if the thread implementation does not cause such an immediate context switch, it is possible for the signalling thread to be suspended after it has called
signal, but before it unlocks the mutex
m. If this happens, the following sequence of events occurs:
While the preceding scenario is functionally correct, it is inefficient because it incurs two extra context switches between the signalling thread and the waiting thread. Because context switches are expensive, this can have quite a large impact on run-time performance, especially if the critical region is small and the condition changes frequently.
Mutex m;
Cond c;
// ...
{
Mutex::Lock sync(m);
// Change some condition other threads may be sleeping on...
} // m is unlocked here
c.signal(); // Signal with the lock available
By arranging the code as shown, you avoid the additional context switches because, when the waiting thread is woken up by the call to
signal, it succeeds in acquiring the mutex before returning from wait without being suspended and woken up again first.
As for monitors, you should exercise caution in using broadcast, particularly if you have many threads waiting on a condition. Condition variables suffer from the same potential problem as monitors with respect to
broadcast, namely, that all threads that are currently suspended inside
wait can immediately attempt to acquire the mutex, but only one of them can succeed and all other threads are suspended again. If your application is sensitive to this condition, you may want to consider waking threads in a more controlled manner, along the lines shown on
page 710