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 a 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:
{zcode:cpp} 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; }; {zcode} |
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:
wait
wait
can be woken up by another thread that calls signal
or broadcast
. When wait
completes, the suspended thread resumes execution with the lock held.timedWait
signal
or broadcast
and wakes up the suspended thread before the timeout expires, the call returns true and the suspended thread resumes execution with the lock held. Otherwise, if the timeout expires, the function returns false. Wait intervals are represented by instances of the Time
class.signal
wait
or timedWait
. If no thread is suspended in a call to wait
or timedWait
at the time signal
is called, the signal is lost (that is, calls to signal
are not remembered if there is no thread to be woken up). Note that signalling does not necessarily run another thread immediately; the thread calling signal
may continue to run. However, depending on the underlying thread library, signal
may also cause an immediate context switch to another thread.broadcast
wait
or timedWait
. As for signal
, calls to broadcast
are lost if no threads are suspended at the time.You must adhere to a few rules for condition variables to work correctly:
wait
or timedWait
unless you hold the lock.wait
call, you must re-test the condition before proceeding, just as for a monitor.In contrast to monitors, which require you to call notify
and notifyAll
with the lock held, condition variables permit you to call signal
and broadcast
without holding the lock. Here is a code example that changes a condition and signals on a condition variable:
{zcode:cpp} Mutex m; Cond c; // ... { Mutex::Lock sync(m); // Change some condition other threads may be sleeping on... c.signal(); // ... } // m is unlocked here {zcode} |
This code is correct and will work as intended, but it is potentially inefficient. Consider the code executed by the waiting thread:
{zcode:cpp} { Mutex::Lock sync(m); while(!condition) { c.wait(sync); } // Condition is now true, do some processing... } // m is unlocked here {zcode} |
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:
wait
and is now woken up by the call to signal
.m
but, because the signalling thread has not yet released the mutex, is suspended again waiting for the mutex to be unlocked.sync
, which unlocks the mutex, making the thread waiting for the mutex runnable.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.
You can avoid the inefficiency by unlocking the mutex before calling signal:
{zcode:cpp} 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 {zcode} |
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.