Update: thanks to Peter for pointing out a glitch in the code supporting the analogy with respect to Pulse/PulseAll. I'd like to say it was a cleverly designed misdirection to see who was paying attention, but I can't :-)
Anyone involved in teaching will tell you that analogy is one of the most effective devices for introducing a new concept to an audience. Describing something new in terms of something the audience already understands greatly reduces learning friction (to invent a phrase) - the amount of resistence a new concept encounters on its way to understanding in the mind of the learner. The trick is to come up an analogy that is both relevant and universally understood by the audience. Explaining the behavior of the C# as operator in terms of the C++ dynamic_cast operator might be a reasonable analogy when presented to a room full of C++ programmers. But it doesn't help (worse, it probably hinders) understanding when used on an audience of Cobol developers.
Over the years, I've used one analogy in particular to introduce the concept of thread synchronization to audiences of developers who were just learning about thread synchronization for the first time: the bathroom pass. I no longer remember when I first used this analogy, but I know my analogy was in place by the time I started teaching people about Win32's CRITICAL_SECTION and mutex constructs. When I started teaching people about the CLR's Monitor construct, the analogy continued to be useful, but the additional functionality of Monitor.Pulse/Wait required that I update the analogy a bit. But the analogy was still a good one, and so the toilet paper extension was introduced.
I first posted this analogy in writing on DevelopMentor's DOTNET-CLR list back in 2003, but the analogy was split over several posts, so I've combined those posts together here and presented them in Q&A form to reflect the nature of the discussion.
What do Monitor.Enter and Monitor.Exit accomplish, and what object reference do I pass to them?
The important part about using SyncLock (VB.NET), lock (C#), or Monitor.Enter is that the threads in question all end up passing a reference to the same object instance. It doesn't particularly matter which object that happens to be - Monitor.Enter (which is called by the SyncLock/lock language wrappers) will only let one thread at a time successfully return from the call to Monitor.Enter; blocking the other threads. Only when the 'winner' releases the lock using Monitor.Exit will another thread be allowed through. The object you choose is largely arbitrary - it just serves as an agreed upon rendezvous point.
Think of locks like you'd think of a bathroom pass that serializes access to a bathroom in a public place (one that doesn't have a lock on the door). In such a scenario, the protocol is that if you want to use the bathroom, you first have to acquire exclusive ownership of the little wooden pass hanging on a hook outside the bathroom door. If you can get possession of that pass, you can proceed to use the bathroom. If you walk down the hall and discover that the bathroom pass is missing, then the protocol dictates that you need to wait until the person holding that lock exits the bathroom and hangs the pass back on the hook; signaling that it's safe for you to proceed. In this manner, any number of people can walk down the hall in an attempt to use the same bathroom, but they'll all wait politely for their turn, which generally helps the situation.
The difficult part is that it's just a protocol - you decide (a) which object will serve as the 'lock' (the bathroom pass) and (b) what resources that lock protects (the bathroom). Then any thread that wants to touch the resource(s) guarded by that lock need to voluntarily grab the right lock (a) before touching the resource, releasing the lock only when they're done using the resource.
Now, since it's a voluntary protocol, there's nothing magical protecting you if any of those threads decide to just skip waiting for the lock and go right ahead and touch the resource directly. They'll be allowed to do so and you'll have undefined results - a difficult-to-reproduce latent bug on your hands that's just waiting for the worst possible moment in your life to show itself (usually when you demo your code to your boss or run it on a real multiprocessor machine, whichever comes first :-).
To return to the analogy, just because you discover that the bathroom pass isn't hanging on the hook doesn't mean you can't just walk right into the bathroom (remember - there's no deadbolt on the door). You're not going to get electrocuted by the doorknob just because someone else is holding the bathroom pass and you're not. Of course, once you actually walk into that bathroom without having first acquired exclusive ownership of the bathroom pass, the results are most definitely undefined.
Similarly, it's important to remember that everyone grab the right lock. To return to the analogy, imagine that this bathroom is one of several in a multistory building (like a dormatory); where each floor has its own bathroom, and each bathroom has its own pass. If you walked down the hall on the 3rd floor and discovered that the bathroom pass was taken, running upstairs to the 4th floor, grabbing that bathroom pass, and then running back down to the 3rd floor and proceeding to enter the bathroom won't do you any good. Yes, you grabbed a bathroom pass - but it wasn't the right one. The 4th floor pass protects the 4th floor bathroom, nothing more.
So always be sure think about which resources need protection against multithreaded access in the first place, and then make sure that everyone is in agreement about which lock will be used to serialize access to that resource.
So when should I use Monitor.Pulse and Monitor.Wait, and how should I use them?
Pulse(All)/Wait are useful when you are not only serializing access to a resource, but additionally might need to wait for that resource to become available (or for some other condition to change). In other words, your goal is not just single-threaded access to a resource, but single-thread access to a resource when the resource is ready PLUS the ability to wait for that resource to become available if it's not (without holding onto the lock while you wait - which is a recipe for deadlock).
Picking up where I left off in my analogy....
Now imagine that you dutifully grab the bathroom pass and proceed to enter the bathroom. However, once you're in the bathroom, you realize (hopefully before progressing too far with things :-) that there's no toilet paper. So you use your cell phone to call dorm maintenance and alert them to situation, and they agree to send someone right up with a new supply of TP. At this point, you need to wait for one of the producer threads (dorm maintenance personnel) to update the resource (TP) before you can proceed.
However, if you just stay in there with the bathroom pass and never put it back on the hook outside the bathroom, the mainteance person (who is following the same protocol as everyone else in line) will not enter the bathroom. This, of course, defeats the purpose of what you were trying to accomplish - and you'll be deadlocked. You're holding the pass and waiting for him to resupply TP, while he's waiting for the pass before bringing the TP into the bathroom.
What you need is a way to wait for the resource to be updated but without retaining possession of the bathroom pass (lock) while you're waiting. But you don't really want to leave the bathroom and restore the pass as if you were done - what you want is to be able to leave the bathroom, restore the pass, but effectively stay in the front of the line that's formed in the hallway while you were waiting (or more accurately, form a new line of people that are waiting for the pass AND the existence of TP in the bathroom).
You do this by calling Monitor.Wait. This method, which you can only call if you are in possession of the specified lock already, releases the lock before putting your calling thread to sleep. You're in a different line (queue) now - not the one that wakes up threads when the lock is available, but the one that wakes up threads when the lock is available and some condition you're waiting on has occurred.
Note that at this point, other people (threads) in line might grab the pass, go into the bathroom, and discover for themselves the same thing you discovered - no TP. If they don't need TP (maybe they're just washing their hands or combing their hair), they'll do their thing and leave, calling Monitor.Exit on the way out (they don't care about TP). But if they do care about TP, then they too will call Monitor.Wait to leave the bathroom and wait (in the other line with you) for maintenance to show up.
At some point, the maintenance guy shows up and, as soon as the pass is available, grabs the pass, enters the bathroom, restocks the TP supply, and then calls Monitor.PulseAll to signal to all waiting threads that he's updated the resource. Having done that, he then calls calls Monitor.Exit to leave the bathroom and replace the pass.
And since he's used PulseAll to signal a change to the resource, as soon as he calls Monitor.Exit to release the lock, waiting threads will return, one at a time, from Monitor.Wait - having reacquired possession of the bathroom pass.[1] You can then proceed to accomplish your task and subsequently call Monitor.Exit when you're done.
[1] A footnote to the above oversimplification
[1] Well, at least in simple terms. If there are other threads waiting in a call to Monitor.Enter while you're waiting in a call to Monitor.Wait, there's no guarantee (that I know of) which says you'll get first dibs on the lock. This is a result of the intricacies of the implementation and some inherent race conditions. What Monitor.Pulse(All) really does is just move the waiting thread(s) from the waiting queue to the ready queue (threads waiting for their turn at ownership of the lock in Monitor.Enter). This is why, in practice, you typically call Monitor.Wait in a loop:
void UseBathroom()
{
lock( this )
{
while( NeedTP() )
{
Monitor.Wait(this);
// You've reacquired the lock,
// but don't assume that means
// there's TP. So loop back &
// check again; re-waiting if
// necessary. If there's still
// TP, you're good to go and will
// exit this loop.
}
UseTP();
}
}
void RestockBathroom()
{
lock( this )
{
RefillTP();
Monitor.PulseAll(this);
}
}
Why did you call PulseAll instead of Pulse above?
It's a function of how many waiting threads you want to notify. For example, in the bathroom analogy, the producer thread (janitor) just calls PulseAll because he wants to signal all of the waiting threads that there's TP available now. By calling PulseAll, the janitor moves all of the waiting threads from the queue they're currently in back over to the queue of threads waiting to use the bathroom. If RestockBathroom above only calls Pulse, only one of the waiting threads will be readied - leaving the others in the wait queue until yet another thread calls Pulse. This can be made to work, but requires that everyone call Pulse right before exiting the bathroom, which is less elegant (and less performant).
Do you really use lock in production code?
No - just in sample code and code compiled by Outlook and/or blogging software. In production code, you should substitute the above use of the C# lock construct, which implies an infinite wait, with the more robust use of Monitor.TryEnter, which accepts a timeout, and some sort of error recover code; something along these lines:
// Janitor/producer thread
if( Monitory.TryEnter(this, 3000) ) { // 3 second timeout.
try {
RefillTP();
Monitor.PulseAll(this);
}
finally {
Monitor.Exit(this);
}
}
else {
DealWithLockAcquisitionFailureHere();
}
Do you really put your braces at the ends of lines like you've done here?
No. I just do that for brevity in email messages & blog posts. In compiled code (sample and production), I put my braces where nature intended: on their own line, indented to line up under the construct for which the braces provide block delineation.
Posted
Dec 13 2004, 07:28 AM
by
mike-woodring