The managed synchronization mechanisms, including Monitor, WaitHandle.WaitAny, ManualResetEvent, ReaderWriterLock, Thread.Join, GC.WaitForPendingFinalizers and the rest of the family are not just a thin platform adaptation layer on top of the Win32 API.
The CLR needs to know exactly which threads are currently waiting for a synchronization mechanisms for a variety of reasons. To mention two of them:
- In hosting scenarios, the CLR host might want to limit the ability of application threads to perform synchronization at all (to ensure reliability and control over threads).
- A waiting thread (in the WaitSleepJoin thread state) won’t be rudely aborted (by Thread.Abort) until it leaves the waiting state.
If all synchronization calls were straight P/Invoke-s to the Win32 services, these restrictions (among others) would be impossible to achieve.
An additional feature enabled by (most) of the managed synchronization mechanisms is message pumping. When you call one of the above APIs in an STA thread, the CLR takes care of message pumping (as of Windows 2000, simply by calling CoWaitForMultipleHandles which calls MsgWaitForMultipleObjects) to ensure that STA COM objects within that STA thread can process incoming calls, which are delivered through the Windows message loop.
If this were not the case, interesting deadlocks could ensue. For example, the finalizer thread might want to release an STA COM object, which requires marshaling the call to the STA thread. If the STA thread has just called GC.WaitForPendingFinalizers and does not pump messages, a nasty deadlock occurs. (There are numerous other examples of how this could be problematic, but I’ll omit them for brevity. Courageous readers are welcome to read Chris Brumme’s post on apartments and pumping.)
However, the CLR is also smart enough to realize that on an MTA thread, there’s no need to pump messages – so it doesn’t. A wait on an MTA thread is a true WaitForMultipleObjects, (almost) no strings attached.
For future reference, here’s what a call stack for an STA thread Thread.Join call looks like (.NET 3.5 x64, edited for brevity):
And here’s what a call stack for an MTA thread Thread.Join call looks like:
Don’t you want to hug these MTA threads? They are SO_TOLERANT.
Oh, and one more thing to wind this up. You may have read that WaitHandle.WaitAll is not supported (at all) on an STA thread, and throws an exception if you attempt to call it. Why does this make sense?
(… Dramatic suspense …)
Well, if you call MsgWaitForMultipleObjects from an STA thread and use bWaitAll=TRUE, you’re essentially saying that you want to wait for all the handles to become signaled and for a message to arrive. This is clearly not the intent, and there’s hardly anything the CLR can do about it, so it forbids the whole situation.
There are workarounds (some described in Chris Brumme’s post which I cited above, some elsewhere), but they don’t fully address the situation. For example, one alternative (which I recently used in a project) is to spawn a new MTA thread, have it perform the WaitHandle.WaitAll, and use Thread.Join to wait for that thread to complete. However, if one of the handles has ownership semantics (e.g. a mutex), this breaks because the mutex will be owned by the wrong (and terminated – thus considered abandoned) thread.
However, one thing to keep in mind is this: The CLR does a whole lot of work to ensure managed synchronization works properly (and employs ruthless dark magic when necessary). The worst thing you can possibly do is calling into Win32 synchronization primitives directly, bypassing the CLR.