What’s the difference between WaitHandle.WaitOne/WaitAny/WaitAll and just PInvoke’ing to WaitForSingleObject or WaitForMultipleObjects directly? Plenty.
There are several reasons why we prefer you to use managed blocking through WaitHandle or similar primitives, rather than calling out to the operating system via PInvoke.
First, we can blur any platform differences for you
Do you know the differences between Windows 95 and Windows Server 2003 when you have duplicate handles in the list you are waiting on? You certainly shouldn’t have to!
Second, we can do any pumping that is appropriate
While a thread in a Single-Threaded Apartment (STA) blocks, we will pump certain messages for you. Message pumping during blocking is one of the black arts at Microsoft. Pumping too much can cause reentrancy that invalidates assumptions made by your application. Pumping too little causes deadlocks. Starting with Windows 2000, OLE32 exposes CoWaitForMultipleHandles so that you can pump “just the right amount.” On lower operating systems, the CLR uses MsgWaitForMultipleHandles / PeekMessage / MsgWaitForMultipleHandlesEx and whatever else is available on that version of the operating system to try to mirror the behavior of CoWaitForMultipleHandles. The net effect is that we will always pump COM calls waiting to get into your STA. And any SendMessages to any windows will be serviced. But most PostMessages will be delayed until you have finished blocking.
The degree of pumping that’s happening has been painfully tuned to be appropriate to WindowsForms, non-GUI console apps, ASP compatibility mode using an STA threadpool on the server, and all the other traditional STA scenarios. However, in the future we know we’re going to be revisiting this. The underlying operating system is evolving and there are some big changes underway in this area. Believe me, you don’t want to be doing this stuff yourself. The CLR should be insulating you from this pain.
Third, the CLR can make wise decisions about activity
The CLR threadpool monitors CPU utilization to guide its heuristics about thread injection and retirement. It also notices GC activity, since there’s little reason to inject a thread that will immediately be suspended until a non-concurrent GC is complete. The threadpool also notices whenever one of its threads is blocked or emerges from a blocking operation. We can do this accurately if you use managed blocking. If you PInvoke to unmanaged blocking services, everything is opaque.
Fourth, we can ensure that your thread can be controlled
The operating system provides a TerminateThread() service. It should never be used under any circumstances. It will corrupt the process. The CLR provides services like Thread.Abort and Thread.Interrupt. They can take control of your thread in a reasonably safe manner. By reasonably safe, I mean that the process and the CLR remain consistent. Your application state might not remain consistent. In particular, if a thread is Aborted while it is executing a .cctor method, I’ve explained in another blog how this leaves your class in an “off limits” situation. Another example of this is that your thread might be Aborted in the middle of executing some backout code like a finally or catch clause. Once again, your application state might be corrupt.
(We’re careful to allow finally and catch clauses to execute once an Abort has been induced on your thread. But that’s subtly different from never inducing an Abort in the middle of a finally or catch execution).
Over time, we hope to provide ways for your application to remain consistent – even in the face of Thread.Abort and other asynchronous exceptions, including resource failures like OutOfMemoryException and StackOverflowException.
Until then, there are only two completely safe uses of Thread.Abort:
1) You can abort your own thread via Thread.CurrentThread.Abort.
2) You can perform an AppDomain.Unload, which internally uses Thread.Abort to unwind threads out of the doomed AppDomain.
The first usage is safe because the Abort isn’t induced asynchronously. You are inducing it directly on your own thread – almost as if you had called “throw new ThreadAbortException();”
The second usage is safe because all the application state is being discarded after the thread has been Aborted. That application state might be inconsistent, but it’s all going away anyway.
However, if you PInvoke to WaitForMultipleObject, then Thread.Abort is powerless. We cannot take control of threads that are in unmanaged code. The operating system provides no safe way to do this. A thread in unmanaged code could be holding arbitrary locks (the OS loader lock and the OS heap lock are two particularly troublesome ones).
So there are several good reasons why you should favor managed blocking over a PInvoke to unmanaged blocking. Examples of managed blocking are:
- Monitor.Enter if there is enough contention for us to give up on spinning and block
Thread.Sleep is a little unusual. We can take control of threads that are inside this service. But, following the tradition of Sleep on the underlying Windows operating system, we perform no pumping.
If you need to Sleep on an STA thread, but you want to perform the standard COM and SendMessage pumping, consider Thread.CurrentThread.Join(timeout) as a replacement.