6
.
An Introduction to Programming with C# Threads
3.3. Waiting for a condition
You can view an object’s lock as a simple kind of resource scheduling
mechanism. The resource being scheduled is the shared memory accessed inside
the “lock” statement, and the scheduling policy is one thread at a time. But often
the programmer needs to express more complicated scheduling policies. This
requires use of a mechanism that allows a thread to block until some condition is
true. In thread systems pre-dating Java, this mechanism was generally called
“condition variables” and corresponded to a separately allocated object [4,11]. In
Java and C# there is no separate type for this mechanism. Instead every object
inherently implements one condition variable, and the “
Monitor” class provides
static “Wait”, “Pulse” and “PulseAll” methods to manipulate an object’s condition
variable.
public sealed class Monitor {
public static bool Wait(Object obj) { … }
public static void Pulse(Object obj) { … }
public static void PulseAll(Object obj) { … }
...
}
A thread that calls “Wait” must already hold the object’s lock (otherwise, the call
of “Wait” will throw an exception). The “Wait” operation atomically unlocks the
object and blocks the thread
*
. A thread that is blocked in this way is said to be
“waiting on the object”. The “Pulse” method does nothing unless there is at least
one thread waiting on the object, in which case it awakens at least one such
waiting thread (but possibly more than one). The “
PulseAll” method is like
“Pulse”, except that it awakens all the threads currently waiting on the object.
When a thread is awoken inside “Wait” after blocking, it re-locks the object, then
returns. Note that the object’s lock might not be immediately available, in which
case the newly awoken thread will block until the lock is available.
If a thread calls “
Wait” when it has acquired the object’s lock multiple times,
the “
Wait” method releases (and later re-acquires) the lock that number of times.
It’s important to be aware that the newly awoken thread might not be the
next thread to acquire the lock: some other thread can intervene. This means that
the state of the variables protected by the lock could change between your call of
“
Pulse” and the thread returning from “Wait”. This has consequences that I’ll
discuss in section 5.
In systems pre-dating Java, the “
Wait” procedure or method took two
arguments: a lock and a condition variable; in Java and C#, these are combined
into a single argument, which is simultaneously the lock and the wait queue. In
terms of the earlier systems, this means that the “
Monitor” class supports only one
condition variable per lock
†
.
*
This atomicity guarantee avoids the problem known in the literature as the “wake-up
waiting” race [14].
†
However, as we’ll see in section 5.2, it’s not very difficult to add the extra semantics
yourself, by defining your own “Condition Variable” class.