STA Objects and the Finalizer Thread: Tale of a Deadlock

June 30, 2010

no comments

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).

namespace ManagedApp
{
  class FinalizableResource
  {
    ISimpleComObject _obj;
    EventWaitHandle _signalWhenDone;

    public FinalizableResource(EventWaitHandle signalWhenDone)
    {
      _obj = new SimpleComObject();
      _signalWhenDone = signalWhenDone;
    }

    ~FinalizableResource()
    {
      //Deadlock here:

      Marshal.FinalReleaseComObject(_obj);

      _signalWhenDone.Set();
    }
  }

  class Program
  {
    [DllImport("kernel32.dll")]
    static extern uint WaitForSingleObject(
        IntPtr handle, uint timeout);

    [STAThread]
    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
     

      //Deadlock here:
      WaitForSingleObject(waitOn.Handle, 100000);
    }
  }
}

(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

ntdll!NtWaitForSingleObject

KERNELBASE!WaitForSingleObjectEx

KERNEL32!WaitForSingleObjectExImplementation

KERNEL32!WaitForSingleObject

0×0

clr!CallDescrWorker

clr!SigParser::GetElemType

clr!MetaSig::MetaSig

0×0

clr!MethodDesc::GetSigFromMetadata

~0s0:002> kc 20

ntdll!NtWaitForSingleObject

KERNELBASE!WaitForSingleObjectEx

KERNEL32!WaitForSingleObjectExImplementation

KERNEL32!WaitForSingleObject

ole32!GetToSTA

ole32!CRpcChannelBuffer::SwitchAptAndDispatchCall

ole32!CRpcChannelBuffer::SendReceive2

ole32!CAptRpcChnl::SendReceive

ole32!CCtxComChnl::SendReceive

ole32!NdrExtpProxySendReceive

RPCRT4!NdrpProxySendReceive

RPCRT4!NdrClientCall2

ole32!ObjectStublessClient

ole32!ObjectStubless

ole32!CObjectContext::InternalContextCallback

ole32!CObjectContext::ContextCallback

clr!CtxEntry::EnterContext

clr!RCW::ReleaseAllInterfacesCallBack

clr!RCW::Cleanup

clr!RCW::FinalExternalRelease

clr!MarshalNative::FinalReleaseComObject

mscorlib_ni

clr!MethodTable::SetObjCreateDelegate

clr!MethodTable::SetObjCreateDelegate

clr!MethodTable::CallFinalizer

clr!WKS::CallFinalizer

clr!WKS::GCHeap::TraceGCSegments

clr!WKS::GCHeap::TraceGCSegments

clr!WKS::GCHeap::FinalizerThreadWorker

clr!Thread::DoExtraWorkForFinalizer

clr!Thread::ShouldChangeAbortToUnload

clr!Thread::ShouldChangeAbortToUnload

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.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>