Consider the following implementation of blocking producer and consumer threads:
static void Main(string[] args)
{
var syncRoot = new object();
var products = new List<int>();
Action producer = () => {
lock (syncRoot)
{
var counter = 0;
while (true)
{
products.Add(counter++);
Monitor.Pulse(syncRoot);
Monitor.Wait(syncRoot);
}
}};
Action consumer = () => {
lock (syncRoot)
while (true)
{
Monitor.Pulse(syncRoot);
products.ForEach(Console.WriteLine);
products.Clear();
Thread.Sleep(500);
Monitor.Wait(syncRoot);
}};
Task.Factory.StartNew(producer);
Task.Factory.StartNew(consumer);
Console.ReadLine();
}
Assuming that when Producing thread enters Monitor.Wait
it waits for two things:
- for pulsing from consumer thread, and
- for reacquiring the lock
In the code above I do my consuming work between Pulse
and Wait
calls.
So if I wrote my consuming thread like this (Pulse immediately before waiting):
Action consumer = () =>
{
lock (syncRoot)
while (true)
{
products.ForEach(Console.WriteLine);
products.Clear();
Thread.Sleep(500);
Monitor.Pulse(syncRoot);
Monitor.Wait(syncRoot);
}
};
I didn't notice any change in behavior. Are there any guidelines to that? Should we generally Pulse
immediately before we Wait
or is there maybe a difference in terms of performance?
Pulse then Wait is nearly identical to Wait then Pulse since they are running in an infinite loop. Pulse,Wait,Pulse,Wait is almost the same as Wait,Pulse,Wait,Pulse
Usually, the thread that is waiting for something to change uses
Wait
and the thread doing the changing usesPulse
. A thread can do both, but general practice depends on the circumstances.The code given is sleeping while holding on to the lock. For learning/simulating purposes that is fine, but production code generally shouldn't do that. You can pass a timeout to
Wait
which is fine and doesn't hold the lock during the wait.Generally speaking, Monitor is considered a low level synchronization primitive and it is advised to use higher level synchronization primitives instead. It's good to understand the low level primitives like Monitor, but the general wisdom is that for almost any practical scenario, some higher level primitive is available that is less error prone, less likely to hide some tricky race scenario, and easier to read in someone else's code.