Search code examples
c#multithreadingwaitmonitorpulse

Using lock, Monitor Pulse and Wait to synchronize threads


I have read the official documentation and about 25 tutorials, but I'm still struggling with how I might synchronize say, 3 threads with Monitor Pulse() and Wait() methods and using lock objects.

(Yes, I know there are other techniques of synchronization, but this should be doable and it's frustrating me)

Here's my simple "proof of concept" idea I came up with.

Let's say I have three threads, each with a task:

  1. Thread1 runs a task that prints out all integers divisible by 3
  2. Thread2 runs a task that prints out all integers whose remainder is 1 when divided by 3
  3. Thread3 runs a task that prints out all integers whose remainder is 2 when divided by 3

I'd like ultimately the output to be: 0,1,2,3,4,5,6,... up to whatever integer limit I might choose but we can say 50 or 100 - it doesn't matter.

I'd like to more fully understand the mechanism of lock vs Monitor.Wait() and Monitor.Pulse() and how they can work together.

If I understand correctly, when a thread encounters a lock(someObject) { ... }, it gains exclusive access to that critical area if it's the first one there. Any other thread that encounters a lock on the same object is stuck on that line (i.e., lock(someObject)) in its respective code, correct?

If thread 1 has the lock(someObject) calls Monitor.Wait(someObject), then the thread 1 releases the lock and enters the waiting queue, correct? Then if any other thread (e.g., thread 2) calls Monitor.Pulse(someObject), it would move the thread1 into the ready queue?

No matter what I try, it seems like the code keeps just waiting/blocking infinitely.

I guess my summary questions are:

  1. Do I need more than one lock object to synchronize three threads using Pulse and Wait?
  2. Where would the Wait and Pulse go in this code? Inside a lock around the loop that is being used to iterate over the values that we want to print? Inside a lock, placed only inside a condition (e.g., if (i % 3 == 2)) ? Etc.

I'm thankful for any helpful input!

UPDATE (8/7/2021):

It turns out making the locks static was necessary given the way I had it set up in a single file. I'm irritated that I didn't notice that earlier, but the online documentation suggested (from the Website of Joe Albahari) was immensely helpful.


Solution

  • Here is a relatively simple Wait/PulseAll example:

    object locker = new();
    int i = 0;
    bool finished = false;
    Thread[] threads = Enumerable.Range(0, 3).Select(remainder => new Thread(() =>
    {
        lock (locker)
        {
            try
            {
                do
                {
                    while (!finished && i % 3 != remainder) Monitor.Wait(locker);
                    if (finished) break;
                    Console.WriteLine($"Worker #{remainder} produced {i}");
                    Monitor.PulseAll(locker);
                } while (++i < 20);
            }
            finally { finished = true; Monitor.PulseAll(locker); }
        }
    })).ToArray();
    Array.ForEach(threads, t => t.Start());
    Array.ForEach(threads, t => t.Join());
    

    Three worker threads are created, identified by the remainder argument, that takes the values 0, 1, and 2. Each worker is responsible for producing numbers whose module 3 equals the remainder.

    The int i is the loop variable, and the bool finished is a flag that becomes true when any worker is finished. This flag ensures that in case of an error in any worker, the other workers will not deadlock.

    Each worker enters a critical section that encloses a do-while loop, which is the number-producing and incrementing loop. Before emitting a number it has to wait for its turn. Its turn comes when the i % 3 == remainder. Otherwise it Waits. When its turn comes, it emits the number, it increments the i, it Pulses all waiting workers, and continues with the next iteration. When the loop ends, it Pulses one last time before releasing the lock.

    The PulseAll has been chosen instead of the Pulse, because we don't know whether the next worker in the waiting queue is the correct one for the current i, so we just wake them all.

    Output:

    Worker #0 produced 0
    Worker #1 produced 1
    Worker #2 produced 2
    Worker #0 produced 3
    Worker #1 produced 4
    Worker #2 produced 5
    Worker #0 produced 6
    Worker #1 produced 7
    Worker #2 produced 8
    Worker #0 produced 9
    Worker #1 produced 10
    Worker #2 produced 11
    Worker #0 produced 12
    Worker #1 produced 13
    Worker #2 produced 14
    Worker #0 produced 15
    Worker #1 produced 16
    Worker #2 produced 17
    Worker #0 produced 18
    Worker #1 produced 19
    

    Try it on fiddle.


    Note: the example in the 1st revision of this answer was problematic, because it was creating an initial busy-wait phase until all workers were ready.