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:
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:
Pulse
and Wait
?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!
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.
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 Wait
s. When its turn comes, it emits the number, it increments the i
, it Pulse
s all waiting workers, and continues with the next iteration. When the loop ends, it Pulse
s 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
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.