1

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.

Ginzorf
  • 769
  • 11
  • 19
  • 1
    You can see what `lock` really is [here](https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpHoAOATgJYBuAhgC4Cm6ZDNwA9gHYA2AnuuwBGAKwYBjOukYBnSQF50nBgHcBI8XQAUASgDchYgaJpMAFnQBZHfiPEi3dmIDW6TTLrbbdgkjt2AvraBSP5AA=). – ProgrammingLlama Aug 06 '21 at 10:47
  • 4
    Do you have any actual use-case in mind? For all intents and purposes you are serializing the work, so why are you using threads in the first place? `Monitor` is close to 20 years old by now, so the 10 year old resources should be perfectly valid. – JonasH Aug 06 '21 at 10:57
  • 1
    The reason why there are no resource less than 10 years old is because nobody uses these synchronization mechanisms anymore, there are more modern practices and libraries – Charlieface Aug 06 '21 at 13:09
  • 1
    There's 8 question marks in your post. Can you limit your questions down to 1? or number them or whatever? If I say "Yes", do you have any idea which particular question I just answered? – Lasse V. Karlsen Aug 06 '21 at 21:26
  • @LasseV.Karlsen - near the end, I list summary questions, and they are numbered. The second one does contain multiple questions, technically, but it's listing possible solutions to demonstrate that I have actually spent time thinking it through - I just haven't reached an actual solution. So the second question is not a yes/no question. An answer might be, "The Wait goes after the _______ and the Pulse goes ______", or a code sample. – Ginzorf Aug 06 '21 at 22:08
  • The third bullet (asking for websites, youtube videos, books etc) should go IMHO, in order for the question to be eligible for reopening. – Theodor Zoulias Aug 07 '21 at 02:58
  • @TheodorZoulias - I have removed it. However, I have always found it peculiar that what questions we are allowed to ask on SO are restricted. It seems that policies reducing the potential for someone to learn more or come closer to the solution to their problem(s) are unhelpful. – Ginzorf Aug 07 '21 at 06:00
  • Hi @Charlieface. Could you please review the question once again, and check whether the reason for being closed still holds? – Theodor Zoulias Aug 08 '21 at 03:42
  • I think it still holds because the question doesn't really make any sense: You start with a "proof of concept" that doesn't prove anything (no apparent need for locking at all). You say "no matter what I try the code keeps just waiting/blocking infinitely" without showing any of that code. Then you ask "Do I need more than one lock object" we have no idea what type of locking you are trying to enforce. Then "Where would the Wait and Pulse go in this code?" we have no way of answering that, we don't know what you are trying to achieve. More the point, we don't even know if `Monitor` is ...... – Charlieface Aug 08 '21 at 04:04
  • ..... the right tool to use anyway, as I pointed out above. You've hardly changed the post since it was closed, no it is absolutely not fit to reopen, sorry. I suggest you try come up with a *specific* question to a *specific* piece of code, and create a new question. – Charlieface Aug 08 '21 at 04:04
  • OK @Charlieface, fair enough. I pinged you because the current reason that the question is closed is: *"We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations."* The OP edited their question and removed the third bullet where they were asking for websites, youtube videos, books etc. They might be able to improve their question even further, based on your feedback. – Theodor Zoulias Aug 08 '21 at 08:44

1 Answers1

1

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104