RCW vs Garbage Collector
Недавно столкнулись с проблемой - вызов 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-объектов из потоков, в которых нет работающего цикла сообщений!
1 коммент.:
This post will assist the internet visitors for building up new blog or even a
blog from start to end.
My page : GFI Norte
Отправить комментарий