Condition variables and monitors are important concurrency and threading abstractions which can help in implementing robust multi-threaded code. Most major programming languages and environments provide implementations of condition variables and monitors out-of-the-box (Java and .NET, for example).
Delphi, unfortunately, doesn’t offer such a functionality and because we needed condition variables and monitors for the Delphi library of our logging tool SmartInspect, we built our own. We decided to share our implementation and thus introduce several classes which add support for condition variables and monitors to Delphi on this page.
Features and Benefits
Locks (often also called mutexes) provide operations to synchronize the access to a region of code which is allowed to be entered by at most one thread at the same time. To enter such a region, a thread must call the enter operation of the lock. If a thread is currently inside the critical region, other threads which might want to the enter the same region will block until the original thread has left the particular code section. To indicate that a thread has left the critical region, it must call the leave operation of the lock.
Condition variables provide operations to wait on a user-defined condition and to signal waiting threads. This makes condition variables very similar to traditional events which can be created and used with the CreateEvent, SetEvent, ResetEvent and PulseEvent windows functions (with TEvent as Delphi wrapper). What’s special about condition variables is the fact that a condition variable is always associated with a related lock. In order to be able to call the wait operation of the condition variable, the caller must hold the related lock. The wait operation then releases the lock and enters the wait state. This is done within an atomic operation. Other threads can then later signal or wake up waiting threads. The related lock is automatically re-acquired before a thread exits the wait operation.
The relationship between condition variables and locks is N:1, i.e. a lock can be associated with multiple condition variables but a condition variable can be used only with a single lock.
Monitors combine the functionality of locks and condition variables into a single data type. They provide the enter and leave operations of locks as well as wait and signal from condition variables. This is done internally by creating an instance of a condition variable and an instance of a related lock. Monitors can come in handy when a single lock and a single condition variable is sufficient.
Uses and Examples
The classic example of condition variables is the producer/consumer problem. One or more threads called producers produce items and add them to a queue. Consumers (other threads) consume items by removing the produced items from the queue.
There are two constraints involved in this example. First, producers should automatically block when the queue is currently considered full. Secondly, consumers are blocked when the queue is currently empty. This can be achieved by introducing two condition variables (one for the empty state and one for the full state) and one lock. Critical regions of code are protected by the lock and producers/consumer are put to sleep and signaled with the two condition variables.
The source code of the Delphi unit (Threads.pas) can be downloaded here. Please see the header of the file for licensing questions. Included in the download is a variant of the producer/consumer problem which demonstrates how to use the lock and condition variable classes. Starting with Windows Vista, Microsoft provides built-in support for condition variables, but earlier versions do not have them. The provided unit doesn’t use the new Windows APIs and should thus work on all Windows versions.
If you have any comments or questions, feel free to leave a comment or to contact me via email at [email protected].