Недавно столкнулись с проблемой - вызов GC.WaitForPendingFinalizers();
блокировал работу приложения. На мысли навела следующая часть стека:
[In a sleep, wait, or join]
ntdll.dll!_KiFastSystemCallRet@0()
ntdll.dll!_NtWaitForMultipleObjects@20() + 0xc bytes
KernelBase.dll!_WaitForMultipleObjectsEx@20() - 0x54 bytes
kernel32.dll!_WaitForMultipleObjectsExImplementation@20() + 0x8e bytes
user32.dll!_RealMsgWaitForMultipleObjectsEx@20() + 0xd7 bytes
ole32.dll!CCliModalLoop::BlockFn() + 0x96 bytes
ole32.dll!_CoWaitForMultipleHandles@20() - 0x51b9 bytes
[Managed to Native Transition]
mscorlib.dll!System.GC.WaitForPendingFinalizers() + 0x2c bytes
Блокировка происходит из ole32.dll
. После просмотра стеков всех
остальных потоков был найден следующий любопытный житель нашего процесса:
> ntdll.dll!_KiFastSystemCallRet@0()
ntdll.dll!_ZwWaitForSingleObject@12() + 0xc bytes
KernelBase.dll!_WaitForSingleObjectEx@12() + 0x6c bytes
kernel32.dll!_WaitForSingleObjectExImplementation@12() + 0x43 bytes
kernel32.dll!_WaitForSingleObject@8() + 0x12 bytes
ole32.dll!GetToSTA() + 0x72 bytes
ole32.dll!CRpcChannelBuffer::SwitchAptAndDispatchCall() - 0x1939 bytes
ole32.dll!CRpcChannelBuffer::SendReceive2() + 0xa6 bytes
ole32.dll!CAptRpcChnl::SendReceive() + 0x5b7 bytes
ole32.dll!CCtxComChnl::SendReceive() - 0x14b97 bytes
ole32.dll!NdrExtpProxySendReceive() + 0x43 bytes
rpcrt4.dll!@NdrpProxySendReceive@4() + 0xe bytes
rpcrt4.dll!_NdrClientCall2() + 0x144 bytes
ole32.dll!_ObjectStublessClient@8() + 0x7a bytes
ole32.dll!_ObjectStubless@0() + 0xf bytes
ole32.dll!CObjectContext::InternalContextCallback() - 0x511f bytes
ole32.dll!CObjectContext::ContextCallback() + 0x8f bytes
clr.dll!CtxEntry::EnterContext() + 0x119 bytes
clr.dll!RCWCleanupList::ReleaseRCWListInCorrectCtx() + 0x2bb bytes
clr.dll!RCWCleanupList::CleanupAllWrappers() - 0x2ef04 bytes
clr.dll!SyncBlockCache::CleanupSyncBlocks() + 0xa6a bytes
clr.dll!Thread::DoExtraWorkForFinalizer() - 0x4c12 bytes
clr.dll!WKS::GCHeap::FinalizerThreadWorker() + 0x8b bytes
clr.dll!Thread::DoExtraWorkForFinalizer() + 0x3e0ff bytes
clr.dll!Thread::ShouldChangeAbortToUnload() - 0x5f4 bytes
clr.dll!Thread::ShouldChangeAbortToUnload() - 0x539 bytes
clr.dll!ManagedThreadBase_NoADTransition() + 0x35 bytes
clr.dll!ManagedThreadBase::FinalizerBase() + 0xf bytes
clr.dll!WKS::GCHeap::FinalizerThreadStart() + 0xfb bytes
clr.dll!Thread::intermediateThreadProc() + 0x48 bytes
kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes
ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes
ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes
Перед нами стек так называемого Finalizer Thread, который выполняет работу по
освобождению ресурсов. Из стека стало ясно, что этот поток пытается избавиться от
ненужных RCW. Для этого он вызывает метод Release
каждого COM-объекта,
но пытается сделать это в другом контексте. Очевидно, что этот контекст - это апартмент
из которого объект попал в CLR. В моем случае поток, соответствующий этому апартменту,
не обрабатывал асинхронные сообщения, а именно ими обменивается ole32.dll
для маршалинга вызовов между контекстами.
Ахтунг состоит в том, что Finalizer Thread является чужеродным контекстом для
всех COM-объектов, которые попали в CLR (за исключением тех, которые были созданы
в контексте самого Finalizer Thread, а это вполне реальная ситуация). Как же в результате
происходит освобождение ресурсов, если вызов
GC.WaitForPendingFinalizers();
блокируется до полной очистки всего накопившегося мусора? Ответ находится на вешине
первого стека:
> CPPHost.exe!CNETFormControllerCallback::~CNETFormControllerCallback() Line 13 C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::~CComObjectNoLock<CNETFormControllerCallback>() Line 3037 + 0xf bytes C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::`scalar deleting destructor'() + 0x2b bytes C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::Release() Line 3049 + 0x35 bytes C++
CPPHost.exe!ATL::_QIThunk::Release() Line 2692 + 0x10 bytes C++
ole32.dll!CRemoteUnknown::DoCallback() + 0x3b bytes
rpcrt4.dll!_Invoke@12() + 0x2a bytes
rpcrt4.dll!_NdrStubCall2@16() + 0x22f bytes
ole32.dll!_CStdStubBuffer_Invoke@12() + 0x70 bytes
ole32.dll!SyncStubInvoke() + 0x34 bytes
ole32.dll!StubInvoke() + 0x7b bytes
ole32.dll!CCtxComChnl::ContextInvoke() + 0xe6 bytes
ole32.dll!MTAInvoke() + 0x1a bytes
ole32.dll!STAInvoke() + 0x4a bytes
ole32.dll!AppInvoke() + 0x92 bytes
ole32.dll!ComInvokeWithLockAndIPID() + 0x27c bytes
ole32.dll!ComInvoke() + 0x71 bytes
ole32.dll!ThreadDispatch() + 0x1a bytes
ole32.dll!ThreadWndProc() + 0xa0 bytes
user32.dll!_InternalCallWinProc@20() + 0x23 bytes
user32.dll!_UserCallWinProcCheckWow@32() + 0xb3 bytes
user32.dll!_DispatchMessageWorker@8() + 0xe6 bytes
user32.dll!_DispatchMessageW@4() + 0xf bytes
ole32.dll!CCliModalLoop::PeekRPCAndDDEMessage() + 0x241 bytes
ole32.dll!CCliModalLoop::BlockFn() - 0x5d50 bytes
ole32.dll!_CoWaitForMultipleHandles@20() - 0x51b9 bytes
[Managed to Native Transition]
mscorlib.dll!System.GC.WaitForPendingFinalizers() + 0x2c bytes
NETLibrary.dll!NETLibrary.NETFormController.GCAndWaitForPendingFinalizers() Line 33 + 0x5 bytes C#
[Native to Managed Transition]
CPPHost.exe!NETLibrary::INETFormController::GCAndWaitForPendingFinalizers() Line 67 + 0x10 bytes C++
CPPHost.exe!wmain(int argc=1, wchar_t * * argv=0x006832d8) Line 107 C++
Здесь видно, что GC.WaitForPendingFinalizers();
блокирует выполнение потока вызовом
CoWaitForMultipleHandles
,
который крутит цикл сообщений для обработки входящих COM-вызовов. Мощно, да?
Допустим, что COM-объект попал в CLR из потока, отличного от того, в котором
происходит вызов GC.WaitForPendingFinalizers();
,
но сам поток к моменту вызова не может обработать входящий вызов (поток завершил
работу, например):
> CPPHost.exe!CNETFormControllerCallback::~CNETFormControllerCallback() Line 13 C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::~CComObjectNoLock<CNETFormControllerCallback>() Line 3037 + 0xf bytes C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::`scalar deleting destructor'() + 0x2b bytes C++
CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::Release() Line 3049 + 0x35 bytes C++
CPPHost.exe!ATL::_QIThunk::Release() Line 2692 + 0x10 bytes C++
clr.dll!ReleaseTransitionHelper() + 0xe bytes
clr.dll!SafeReleaseHelper() + 0x6b bytes
clr.dll!SafeRelease() + 0x2f bytes
clr.dll!IUnkEntry::Free() + 0x4e bytes
clr.dll!RCW::ReleaseAllInterfaces() + 0x1a bytes
clr.dll!RCW::ReleaseAllInterfacesCallBack() + 0x178b5c bytes
clr.dll!RCW::Cleanup() + 0x22 bytes
clr.dll!RCWCleanupList::ReleaseRCWListRaw() + 0x18 bytes
clr.dll!RCWCleanupList::ReleaseRCWListInCorrectCtx() + 0x17a15e bytes
clr.dll!RCWCleanupList::CleanupAllWrappers() - 0x2ef04 bytes
clr.dll!SyncBlockCache::CleanupSyncBlocks() + 0xa6a bytes
clr.dll!Thread::DoExtraWorkForFinalizer() - 0x4c12 bytes
clr.dll!WKS::GCHeap::FinalizerThreadWorker() + 0x8b bytes
clr.dll!Thread::DoExtraWorkForFinalizer() + 0x3e0ff bytes
clr.dll!Thread::ShouldChangeAbortToUnload() - 0x5f4 bytes
clr.dll!Thread::ShouldChangeAbortToUnload() - 0x539 bytes
clr.dll!ManagedThreadBase_NoADTransition() + 0x35 bytes
clr.dll!ManagedThreadBase::FinalizerBase() + 0xf bytes
clr.dll!WKS::GCHeap::FinalizerThreadStart() + 0xfb bytes
clr.dll!Thread::intermediateThreadProc() + 0x48 bytes
kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes
ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes
ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes
В таком случае COM-объект освобождается прямо в контексте Finalizer Thread, что,
понятное дело, может привести к проблемам синхронизации, а то и вовсе катастрофой,
если код оъекта был выгружен из памяти!
Последний случай - поток работает, в нем инициализирован апартмент, но он не
обрабатывает асинхронные сообщения, Finalizer Thread рано или поздно подвисает...
Какие выводы можно сделать из всего этого?
- CLR пытается быть a good COM citizen и выполняет автоматический маршалинг
вызовов в нужные контексты;
- из-за специфики среды выполнения разрушение RCW происходит в выделенном
потоке, поэтому во время этой операции за кадром выполняется множество действий,
которые могут привести к блокировкам;
- уничтожением RCW Finalizer Thread занимается уже после того, как были вызваны
методы
Finalize
объектов в соответствующей очереди, поэтому во время
вызова
Finalize
можно пользоваться RCW, но очень нежелательно (блокировки,
заваленные вызовы QueryInterface
если tlb
не зарегистрирована
и т.д.);
- блокировать поток желательно вызовом
CoWaitForMultipleHandles
;
- Избегать передачи COM-объектов из потоков, в которых нет работающего цикла
сообщений!