Here’s a non-trivial deadlock that manifests from using a non-pumping wait API and a finalizer. It is another example of why finalizers are a dangerous cleanup mechanism and why you should avoid them at all costs.
Let’s say that you have an STA COM object called NativeComObject that your managed application is using, and you wrap the COM object with a class called FinalizableResource. This latter class has a finalizer that cleans up resources associated with the COM object by calling a cleanup method on it, or even by deterministically releasing the object with Marshal.FinalReleaseComObject.
Note that the object is STA, meaning that if you created it in an application thread, the finalizer thread won’t be able to access the object directly—it will have to send a Windows message to the object’s STA thread and use it to call the method. This completes the picture of a possible deadlock—if the STA thread waits for a resource acquired by the finalizer thread, and the finalizer thread performs a COM method call into the STA, the two threads are blocked waiting for one another.
Fortunately, most .NET synchronization APIs use the moral equivalent of MsgWaitForMultipleObjects (or CoWaitForMultipleHandles), which are APIs that perform message pumping while waiting. However, if you resort to native synchronization APIs (for example, if your STA thread is now in unmanaged code which uses a wait API), you might encounter this deadlock.
This is some sample code that reproduces the problem (assuming, of course, that you have an STA COM object called SimpleComObject on your hands).
public FinalizableResource(EventWaitHandle signalWhenDone)
_obj = new SimpleComObject();
_signalWhenDone = signalWhenDone;
static extern uint WaitForSingleObject(
IntPtr handle, uint timeout);
static void Main(string args)
ManualResetEvent waitOn = new ManualResetEvent(false);
FinalizableResource r = new FinalizableResource(waitOn);
r = null;
GC.Collect(); //The finalizer will be called soon
(Note that the “r = null” line might seem redundant because the local variable is no longer used after the line where it is declared, but in Debug builds, local variables are considered GC roots until the end of the scope.)
Here’s what it looks like in the debugger:
0:000> kc 20
~0s0:002> kc 20
I.e, the main thread is calling WaitForSingleObject directly, and the finalizer thread, in its attempt to release a COM object, needs to perform a cross-thread call to the STA thread. Both threads are waiting for each other.