C# Producer-Consumer pattern with Monitor.Wait and Monitor.Pulse

1.6k views Asked by At

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:

  1. for pulsing from consumer thread, and
  2. 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?

1

There are 1 answers

0
user2684301 On
  • 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 uses Pulse. 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.