Managed Synchronization Primitives and Thread Apartment States
Join the DZone community and get the full member experience.
Join For FreeThe 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):
0:006> k
ntdll!NtWaitForMultipleObjects+0xa
KERNEL32!WaitForMultipleObjectsEx+0x10b
USER32!RealMsgWaitForMultipleObjectsEx+0x129
USER32!MsgWaitForMultipleObjectsEx+0x46
ole32!CCliModalLoop::BlockFn+0xbb
ole32!CoWaitForMultipleHandles+0x145
mscorwks!NT5WaitRoutine+0x77
mscorwks!MsgWaitHelper+0xed
mscorwks!Thread::DoAppropriateAptStateWait+0x67
mscorwks!Thread::DoAppropriateWaitWorker+0x195
mscorwks!Thread::DoAppropriateWait+0x5c
mscorwks!Thread::JoinEx+0xa5
mscorwks!ThreadNative::DoJoin+0xda
mscorwks!ThreadNative::Join+0xfa
0x642`80150b01
And here’s what a call stack for an MTA thread Thread.Join call looks like:
0:005> k
ntdll!NtWaitForMultipleObjects+0xa
KERNEL32!WaitForMultipleObjectsEx+0x10b
mscorwks!WaitForMultipleObjectsEx_SO_TOLERANT+0xc1
mscorwks!Thread::DoAppropriateAptStateWait+0x41
mscorwks!Thread::DoAppropriateWaitWorker+0x195
mscorwks!Thread::DoAppropriateWait+0x5c
mscorwks!Thread::JoinEx+0xa5
mscorwks!ThreadNative::DoJoin+0xda
mscorwks!ThreadNative::Join+0xfa
0x642`80150a86
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.
Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Implementing a Serverless DevOps Pipeline With AWS Lambda and CodePipeline
-
Essential Architecture Framework: In the World of Overengineering, Being Essential Is the Answer
-
Database Integration Tests With Spring Boot and Testcontainers
-
Building a Flask Web Application With Docker: A Step-by-Step Guide
Comments