用静态存储持续时间破坏本地对象

2012-12-09总结:

  • 在普通混合模式应用程序中,全局本地C ++析构函数作为终结器运行。 无法更改该行为或关联的超时。
  • 混合模式程序集DLL在DLL加载/卸载期间运行C ++构造函数/析构函数 – 与原生DLL完全一样。
  • 使用COM接口将CLR托pipe在本地可执行文件中,可以使解构器在本地DLL(我期望的行为)中运行,并为终结器设置超时(额外奖励)。
  • 据我可以告诉上面适用于至lessVisual Studio 2008,2010和2012.(只testing与.NET 4)

我打算使用的实际CLR托pipe可执行文件与此问题中列出的可执行文件非常相似,只是进行了一些小的更改:

  • 按照Hans Passant的build议,将OPR_FinalizerRun设置为某个值(目前为60秒,但可能会更改)。
  • 使用ATL COM智能指针类(这些在Visual Studio的高级版本中是不可用的,所以我在这篇文章中省略了它们)。
  • Lodaing从mscoree.dlldynamicCLRCreateInstance (以便在安装兼容CLR时允许更好的错误消息)。
  • 将命令行从主机传递到程序集DLL中指定的Main函数。

感谢所有花时间阅读问题和/或评论的人。


2012-12-02更新在post底部。

我正在使用Visual Studio 2012与.NET 4混合模式C ++ / CLI应用程序,惊奇地发现一些本地全局对象的析构函数没有被调用。 调查这个问题,事实certificate,他们的行为像本文所解释的托pipe对象。

我对这种行为感到非常惊讶(我理解它是用于pipe理对象的),无论是在C ++ / CLI标准还是在析构函数和终结器的描述中,都找不到任何地方。

在Hans Passant的评论中,我把这些程序编译成一个程序集DLL,并把它存放在一个小的本地可执行文件中,它给了我所期望的行为(析构函数有足够的时间在同一个线程中完成并运行build)!

我的问题:

  1. 我可以在独立的可执行文件中获得相同的行为吗?
  2. 如果(1)不可行,是否可以configuration进程超时策略(即基本上调用ICLRPolicyManager->SetTimeout(OPR_ProcessExit, INFINITE) )为可执行文件? 这将是一个可接受的解决方法。
  3. 这是在哪里logging/我怎样才能更多地教育自己的话题? 我宁愿不依赖于可能改变的行为。

重新编译下面的文件如下:

 cl /EHa /MDd CLRHost.cpp cl /EHa /MDd /c Native.cpp cl /EHa /MDd /c /clr CLR.cpp link /out:CLR.exe Native.obj CLR.obj link /out:CLR.dll /DLL Native.obj CLR.obj 

不想要的行为:

 C:\Temp\clrhost>clr.exe [1210] Global::Global() [d10] Global::~Global() C:\Temp\clrhost> 

运行托pipe:

 C:\Temp\clrhost>CLRHost.exe clr.dll [1298] Global::Global() 2a returned. [1298] Global::~Global() [1298] Global::~Global() - Done! C:\Temp\clrhost> 

使用的文件:

 // CLR.cpp public ref class T { static int M(System::String^ arg) { return 42; } }; int main() {} // Native.cpp #include <windows.h> #include <iostream> #include <iomanip> using namespace std; struct Global { Global() { wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::Global()" << endl; } ~Global() { wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::~Global()" << endl; Sleep(3000); wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::~Global() - Done!" << endl; } } g; // CLRHost.cpp #include <windows.h> #include <metahost.h> #pragma comment(lib, "mscoree.lib") #include <iostream> #include <iomanip> using namespace std; int wmain(int argc, const wchar_t* argv[]) { HRESULT hr = S_OK; ICLRMetaHost* pMetaHost = 0; ICLRRuntimeInfo* pRuntimeInfo = 0; ICLRRuntimeHost* pRuntimeHost = 0; wchar_t version[MAX_PATH]; DWORD versionSize = _countof(version); if (argc < 2) { wcout << L"Usage: " << argv[0] << L" <assembly.dll>" << endl; return 0; } if (FAILED(hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost)))) { goto out; } if (FAILED(hr = pMetaHost->GetVersionFromFile(argv[1], version, &versionSize))) { goto out; } if (FAILED(hr = pMetaHost->GetRuntime(version, IID_PPV_ARGS(&pRuntimeInfo)))) { goto out; } if (FAILED(hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost)))) { goto out; } if (FAILED(hr = pRuntimeHost->Start())) { goto out; } DWORD dwRetVal = E_NOTIMPL; if (FAILED(hr = pRuntimeHost->ExecuteInDefaultAppDomain(argv[1], L"T", L"M", L"", &dwRetVal))) { wcerr << hex << hr << endl; goto out; } wcout << dwRetVal << " returned." << endl; if (FAILED(hr = pRuntimeHost->Stop())) { goto out; } out: if (pRuntimeHost) pRuntimeHost->Release(); if (pRuntimeInfo) pRuntimeInfo->Release(); if (pMetaHost) pMetaHost->Release(); return hr; } 

2012-12-02
据我所知,行为似乎如下:

  • 在混合模式EXE文件中,全局析构函数在DomainUnload期间作为终结器运行, 而不pipe它们是放置在本机代码还是CLR代码中 。 在Visual Studio 2008,2010和2012中就是这种情况。
  • 在由本地应用程序托pipe的混合模式DLL中,在托pipe方法运行并发生所有其他清理后,会在DLL_PROCESS_DETACH期间运行全局本机对象的析构函数。 他们运行在与构造函数相同的线程中,并且没有与它们相关的超时(期望​​的行为)。 正如所料,全局托pipe对象 (放置在使用/clr编译的文件中的非引用类)的时间析构函数可以使用ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>)

有一个猜测,我认为全局本地构造函数/析构函数在DLL场景中“正常”(定义为行为与我所期望的一样)的原因是允许在本机函数上使用LoadLibraryGetProcAddress 。 因此,我希望在可以预见的将来,依靠它是不会改变的,是相对安全的,但是希望能以任何方式从官方来源/文件中得到某种确认/否认。

更新2

在Visual Studio 2012中(使用快速和高级版本进行testing,我不幸无法访问此机器上的早期版本)。 它应该以相同的方式在命令行(build立如上所述),但是这里是如何从IDE内重现。

build立CLRHost.exe:

  1. 文件 – >新build项目
  2. Visual C ++ – > Win32 – > Win32控制台应用程序(将项目命名为“CLRHost”)
  3. 应用程序设置 – >附加选项 – >空项目
  4. 按“完成”
  5. 右键单击解决scheme资源pipe理器中的源文件。 添加 – >新build项目 – > Visual C ++ – > C ++文件。 将其命名为CLRHost.cpp并从post中粘贴CLRHost.cpp的内容。
  6. 项目 – >属性。 configuration属性 – > C / C ++ – >代码生成 – >将“启用C ++exception”更改为“有SEHexception(/ EHa)”和“基本运行时检查”为“默认”
  7. build立。

build立CLR.DLL:

  1. 文件 – >新build项目
  2. Visual C ++ – > CLR – >类库(名称项目“CLR”)
  3. 删除所有自动生成的文件
  4. 项目 – >属性。 configuration属性 – > C / C ++ – >预编译头文件 – >预编译头文件。 更改为“不使用预编译头”。
  5. 右键单击解决scheme资源pipe理器中的源文件。 添加 – >新build项目 – > Visual C ++ – > C ++文件。 将其命名为CLR.cpp,并从post中粘贴CLR.cpp的内容。
  6. 添加一个名为Native.cpp的新C ++文件并粘贴post中的代码。
  7. 右键单击解决scheme资源pipe理器中的“Native.cpp”,然后select属性。 将C / C ++ – > General – > Common Language RunTime支持更改为“No Common Language RunTime Support”
  8. 项目 – >属性 – >debugging。 将“Command”指向CLRhost.exe,将“Command Arguments”改为“$(TargetPath)”,包括引号,“debugging器types”改为“Mixed”
  9. build立和debugging。

在Global的析构函数中放置一个断点给出下面的堆栈跟踪:

 > clr.dll!Global::~Global() Line 11 C++ clr.dll!`dynamic atexit destructor for 'g''() + 0xd bytes C++ clr.dll!_CRT_INIT(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 416 C clr.dll!__DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 522 + 0x11 bytes C clr.dll!_DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 472 + 0x11 bytes C mscoreei.dll!__CorDllMain@12() + 0x136 bytes mscoree.dll!_ShellShim__CorDllMain@12() + 0xad bytes ntdll.dll!_LdrpCallInitRoutine@16() + 0x14 bytes ntdll.dll!_LdrShutdownProcess@0() + 0x141 bytes ntdll.dll!_RtlExitUserProcess@4() + 0x74 bytes kernel32.dll!74e37a0d() mscoreei.dll!RuntimeDesc::ShutdownAllActiveRuntimes() + 0x10e bytes mscoreei.dll!_CorExitProcess@4() + 0x27 bytes mscoree.dll!_ShellShim_CorExitProcess@4() + 0x94 bytes msvcr110d.dll!___crtCorExitProcess() + 0x3a bytes msvcr110d.dll!___crtExitProcess() + 0xc bytes msvcr110d.dll!__unlockexit() + 0x27b bytes msvcr110d.dll!_exit() + 0x10 bytes CLRHost.exe!__tmainCRTStartup() Line 549 C CLRHost.exe!wmainCRTStartup() Line 377 C kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes 

作为一个独立的可执行文件运行,我得到一个与Hans Passant观察到的非常相似的堆栈跟踪(虽然它没有使用CRT的可pipe理版本):

 > clrexe.exe!Global::~Global() Line 10 C++ clrexe.exe!`dynamic atexit destructor for 'g''() + 0xd bytes C++ msvcr110d.dll!__unlockexit() + 0x1d3 bytes msvcr110d.dll!__cexit() + 0xe bytes [Managed to Native Transition] clrexe.exe!<CrtImplementationDetails>::LanguageSupport::_UninitializeDefaultDomain(void* cookie) Line 577 C++ clrexe.exe!<CrtImplementationDetails>::LanguageSupport::UninitializeDefaultDomain() Line 594 + 0x8 bytes C++ clrexe.exe!<CrtImplementationDetails>::LanguageSupport::DomainUnload(System::Object^ source, System::EventArgs^ arguments) Line 628 C++ clrexe.exe!<CrtImplementationDetails>::ModuleUninitializer::SingletonDomainUnload(System::Object^ source, System::EventArgs^ arguments) Line 273 + 0x6e bytes C++ kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes 

首先获得简单的问题:

CLR定制的一个很好的资源是Steven Pratschner的书 “自定义Microsoft .NET Framework公共语言运行时”。 注意它已经过时了,.NET 4.0中的主机接口已经改变了。 MSDN并没有多说这个,但是托管接口已经有很好的文档了。

您可以通过更改调试器设置使调试更简单,将Type从“Auto”更改为“Managed”或“Mixed”。

请注意,您的3000毫秒睡眠只是在边缘,你应该测试5000毫秒。 如果C ++类出现在使用/ clr生效的代码中, 即使#pragma是非托管的,你也需要重载终结器线程超时。 在.NET 3.5 SP1 CLR版本上测试,下面的代码运行良好,给析构函数足够的时间运行完成:

 ICLRControl* pControl; if (FAILED(hr = pRuntimeHost->GetCLRControl(&pControl))) { goto out; } ICLRPolicyManager* pPolicy; if (FAILED(hr = pControl->GetCLRManager(__uuidof(ICLRPolicyManager), (void**)&pPolicy))) { goto out; } hr = pPolicy->SetTimeout(OPR_FinalizerRun, 60000); pPolicy->Release(); pControl->Release(); 

我在合理的时间选了一分钟,根据需要调整。 请注意MSDN文档有一个错误,它不显示OPR_FinalizerRun作为允许的值,但它确实工作正常。 设置终结器线程超时还可以确保托管终结器在间接破坏非托管C ++类(一种非常常见的情况)时不会超时。

当你用CLRHost编译/ clr运行这个代码时,你会看到一个事情,就是对GetCLRManager()的调用将失败,并返回一个HOST_E_INVALIDOPERATION返回码。 被装载来执行CLRHost.exe的默认CLR主机不会让你重写策略。 所以你坚持有一个专门的EXE来托管CLR。

当我通过使用CLRHost加载混合模式程序集来测试这个时,调用堆栈在析构函数上设置断点时看起来像这样:

 CLRClient.dll!Global::~Global() Line 24 C++ [Managed to Native Transition] CLRClient.dll!<modulee>.?A0x789967ab.??__Fg@@YMXXZ() + 0x1b bytes CLRClient.dll!_exit_callback() Line 449 C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::_UninitializeDefaultDomain(void* cookie = <undefined value>) Line 753 C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::UninitializeDefaultDomain() Line 775 + 0x8 bytes C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::DomainUnload(System::Object^ source = 0x027e1274, System::EventArgs^ arguments = <undefined value>) Line 808 C++ msvcm90d.dll!<CrtImplementationDetails>.moduleeUninitializer.SingletonDomainUnload(object source = {System.AppDomain}, System.EventArgs arguments = null) + 0xa1 bytes // Rest omitted 

请注意,这不像你的问题中的意见。 该代码由CRT的管理版本(msvcm90.dll)触发。 而这段代码运行在一个专用的线程上,由CLR启动来卸载一个appdomain。 你可以在vc / crt / src / mstartup.cpp源代码文件中看到这个源代码。


第二种情况发生在C ++类是编译时没有/ clr的源代码文件的一部分,并被链接到混合模式程序集中。 编译器然后使用正常的atexit()处理程序来调用析构函数,就像通常在非托管可执行文件中所做的那样。 在这种情况下,DLL在程序终止时被Windows卸载,并且管理的CRT版本关闭。

值得注意的是,这发生 CLR关闭之后 ,并且析构函数在程序的启动线程上运行。 因此,CLR超时在图片之外,并且析构函数可以花费尽可能长的时间。 现在堆栈跟踪的本质是:

 CLRClient.dll!Global::~Global() Line 12 C++ CLRClient.dll!`dynamic atexit destructor for 'g''() + 0xd bytes C++ // Confusingly named functions elided //... CLRHost.exe!__crtExitProcess(int status=0x00000000) Line 732 C CLRHost.exe!doexit(int code=0x00000000, int quick=0x00000000, int retcaller=0x00000000) Line 644 + 0x9 bytes C CLRHost.exe!exit(int code=0x00000000) Line 412 + 0xd bytes C // etc.. 

然而,这只是在启动EXE不受管理的情况下才会出现的情况。 一旦EXE被管理,它将在AppDomain.Unload上运行析构函数,即使它们出现在编译时没有/ clr的代码中。 所以你仍然有超时问题。 拥有一个非托管的EXE并不是很少见,例如,当你加载[ComVisible]托管代码时会发生这种情况。 但是,这听起来不像你的情况,你坚持与CLRHost。

为了回答“这个记录在哪里/我怎样才能更多地了解这个话题?” 问题:如果您从这里下载并查看共享源公共语言基础结构 (又名SSCLI),则可以理解这是如何工作的(或者至少用于框架2的工作) http://www.microsoft.com/zh-cn/ us / download / details.aspx?id = 4917 。

一旦你提取了这些文件,你会在gcEE.ccp (“垃圾回收执行引擎”)中找到这个:

 #define FINALIZER_TOTAL_WAIT 2000 

它定义了这个着名的默认值2秒。 你也将在同一个文件中看到这个:

 BOOL GCHeap::FinalizerThreadWatchDogHelper() { // code removed for brevity ... DWORD totalWaitTimeout; totalWaitTimeout = GetEEPolicy()->GetTimeout(OPR_FinalizerRun); if (totalWaitTimeout == (DWORD)-1) { totalWaitTimeout = FINALIZER_TOTAL_WAIT; } 

这将告诉您执行引擎将遵循OPR_FinalizerRun策略(如果已定义),它们对应于EClrOperation枚举中的值。 GetEEPolicy在eePolicy.heePolicy.cpp定义。