我最初很想删除这个问题,但我想我会分享我的发现,以防其他一些可怜的灵魂遇到同样的问题。这个问题非常神秘,在我的应用程序的多次运行中从未发生过,并且仅在测试时发生,并且从未在调试测试时发生。
这个看起来很无辜的功能
const char* BUILD_DATE = __DATE__;
std::stringstream ss(BUILD_DATE);
std::string month;
size_t year;
ss >> month;
ss >> year;
ss >> year;
在 C++/CLI dll 中实现。在我详细介绍之前,让我解释一下 stringstream 如何在这里读取月份和年份。要弄清楚有多少个字符构成月份变量 ss >> month
,需要用空格分隔 ss 字符串缓冲区。它使用当前语言环境的方式,特别是它的一个方面,称为ctype。ctype facet 有一个叫做ctype::is 的函数,它可以判断一个字符是否是空格。在一个表现良好的 C++ 应用程序中,一切都按照标准工作。现在让我们假设由于某种原因ctype facet被破坏了。中提琴,operator >>
无法确定什么是空格,什么不是空格,也无法正确解析。这正是我的情况所发生的事情,以下是详细信息。
其余答案仅适用于 Visual Studio 2010 提供的 std c++ 库及其在 C++/CLI 下的运行方式。
考虑一些这样的代码
struct Foo
{
Foo()
{
x = 42;
}
~Foo()
{
x = 45;
}
int x;
};
Foo myglobal;
void SimpleFunction()
{
int myx = myglobal.x;
}
int main()
{
SimpleFunction();
return 0;
}
这里myglobal是你所说的对象,静态存储持续时间保证在进入 main 之前被初始化,在SimpleFunction中你总是会看到myx
为42。myglobal的生命周期是我们通常所说的每个进程,因为它在问题的生命周期内有效。Foo::~Foo析构函数只会在 main 返回后运行。
输入 C++/CLI 和 AppDomain
根据 msdn 的AppDomain为您提供了应用程序执行的隔离环境。对于 C++/CLI,它引入了我称之为appdomain 存储持续时间的对象的概念
__declspec(appdomain) Foo myglobal;
因此,如果您像上面那样更改了 myglobal 的定义,您可能会使 myglobal.x 在不同的应用程序域中成为不同的值,例如线程本地存储。因此,静态持续时间的常规 C++ 对象在程序的初始化/退出期间被初始化/清理。我在这里非常松散地使用 init/exit/cleaned,但你明白了。在加载/卸载 AppDomain 期间初始化/清理 appdomain 存储对象。
典型的托管程序仅使用默认的 AppDomain,因此每个进程/每个应用程序域的存储几乎相同。
在 C++ 中,静态初始化顺序失败是一个非常常见的错误,其中静态存储持续时间的对象在初始化期间引用其他可能尚未初始化的静态存储持续时间的对象。
现在考虑当每个进程变量引用每个域变量时会发生什么。基本上在卸载 AppDomain 之后,每个进程的变量将引用垃圾内存。对于那些想知道它与原始问题有什么关系的人,请多容忍我一点。
Visual Studio use_facet 实现
std::use_facet用于从语言环境中获取感兴趣的方面。它用于operator <<
获取ctype
刻面。它被定义为
template <class Facet> const Facet& use_facet ( const locale& loc );
请注意,它返回对Facet的引用。VC实现的方式是
const _Facet& __CRTDECL use_facet(const locale& _Loc)
{ // get facet reference from locale
_BEGIN_LOCK(_LOCK_LOCALE) // the thread lock, make get atomic
const locale::facet *_Psave =
_Facetptr<_Facet>::_Psave; // static pointer to lazy facet
size_t _Id = _Facet::id;
const locale::facet *_Pf = _Loc._Getfacet(_Id);
if (_Pf != 0)
; // got facet from locale
else if (_Psave != 0)
_Pf = _Psave; // lazy facet already allocated
else if (_Facet::_Getcat(&_Psave, &_Loc) == (size_t)(-1))
#if _HAS_EXCEPTIONS
_THROW_NCEE(bad_cast, _EMPTY_ARGUMENT); // lazy disallowed
#else /* _HAS_EXCEPTIONS */
abort(); // lazy disallowed
#endif /* _HAS_EXCEPTIONS */
else
{ // queue up lazy facet for destruction
_Pf = _Psave;
_Facetptr<_Facet>::_Psave = _Psave;
locale::facet *_Pfmod = (_Facet *)_Psave;
_Pfmod->_Incref();
_Pfmod->_Register();
}
return ((const _Facet&)(*_Pf)); // should be dynamic_cast
_END_LOCK()
}
这里发生的是我们向语言环境询问感兴趣的方面并将其存储在
template<class _Facet>
struct _Facetptr
{ // store pointer to lazy facet for use_facet
__PURE_APPDOMAIN_GLOBAL static const locale::facet *_Psave;
};
本地缓存_Psave以便后续调用获取相同方面更快。use_facet 的调用者不负责返回的分面生命周期管理,那么这些分面是如何清理的。秘诀是代码的最后一部分,带有注释排队延迟方面进行破坏。最终_Pfmod->_Register()
称之为
__PURE_APPDOMAIN_GLOBAL static _Fac_node *_Fac_head = 0;
static void __CLRCALL_OR_CDECL _Fac_tidy()
{ // destroy lazy facets
_BEGIN_LOCK(_LOCK_LOCALE) // prevent double delete
for (; std::_Fac_head != 0; )
{ // destroy a lazy facet node
std::_Fac_node *nodeptr = std::_Fac_head;
std::_Fac_head = nodeptr->_Next;
_DELETE_CRT(nodeptr);
}
_END_LOCK()
}
struct _Fac_tidy_reg_t { ~_Fac_tidy_reg_t() { ::_Fac_tidy(); } };
_AGLOBAL const _Fac_tidy_reg_t _Fac_tidy_reg;
void __CLRCALL_OR_CDECL locale::facet::_Facet_Register(locale::facet *_This)
{ // queue up lazy facet for destruction
_Fac_head = _NEW_CRT _Fac_node(_Fac_head, _This);
}
很聪明的权利。将所有新的方面添加到链接列表中,并使用静态对象析构函数将它们全部清除。除了有一点小问题。_Fac_tidy_reg
被标记为 _AGLOBAL 意味着所有创建的方面都在每个应用程序域级别上被销毁。
locale::facet *_Psave
另一方面,声明似乎__PURE_APPDOMAIN_GLOBAL
最终扩展为per-process的含义。因此,在清理 appdomain 后,per-process _Psave
可能会指向已删除的多面内存。这正是我的问题。VS2010 单元测试发生的方式是一个名为QTAgent的进程运行你的所有测试。这些测试似乎是在不同的应用程序域中由同一个QTAgent在不同的运行中完成的过程。最有可能隔离先前测试运行的副作用以影响后续测试。对于几乎所有静态存储都是线程/应用程序域级别的完全托管代码来说,这一切都很好,但是对于错误地使用每个进程/每个应用程序域的 C++/CLI,这可能是一个问题。我永远无法调试测试并发现问题的原因是因为 UT 基础设施似乎总是产生一个新的QTAgent进程进行调试,这意味着一个新的 appdomain 和一个没有这些问题的新进程。