STA Objects and the Finalizer Thread: Tale of a Deadlock
Join the DZone community and get the full member experience.
Join For FreeHere’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
0x0
clr!CallDescrWorker
clr!SigParser::GetElemType
clr!MetaSig::MetaSig
0x0
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.
Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Logging Best Practices Revisited [Video]
-
Merge GraphQL Schemas Using Apollo Server and Koa
-
Why You Should Consider Using React Router V6: An Overview of Changes
-
MLOps: Definition, Importance, and Implementation
Comments