我知道这是一个非常冗长的问题。我试图将问题与我的调查分开,以便更容易阅读。
我正在使用MSTest.exe运行单元测试。偶尔会出现这个测试错误:
在单个单元测试方法中:“代理进程在运行测试时停止了。”
在整个测试运行期间:
其中一个后台线程引发异常: System.NullReferenceException:对象引用未设置为对象实例。 在System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o) 在System.Management.Instrumentation.MetaDataInfo.Dispose() 在System.Management.Instrumentation.MetaDataInfo.Finalize()
因此,这是我认为我需要做的事情:我需要找到导致MetaDataInfo错误的原因,但我一筹莫展。我的单元测试套件需要超过半个小时才能运行,并且错误并不总是发生,因此很难复制它。
有其他人看到过这种运行单元测试失败的情况吗?您能否将其跟踪到特定组件?
编辑:
被测试的代码是C#、C++/CLI和一小部分不受管理的C++代码的混合物。未受管理的C++仅从C++/CLI中使用,从未直接从单元测试中使用。单元测试全部是C#。
被测试的代码将在独立的Windows服务中运行,因此没有来自ASP.net或任何类似东西的复杂性。在被测试的代码中,有线程启动和停止、网络通信以及对本地硬盘的文件I/O。
我的调查:
我花了一些时间在我的Windows 7机器上挖掘多个版本的System.Management程序集,并发现在我的Windows目录中具有MetaDataInfo类的System.Management。 (位于Program Files \ Reference Assemblies下的版本要小得多,而且没有MetaDataInfo类。)
使用Reflector检查这个程序集,我发现MetaDataInfo.Dispose()中似乎存在明显的错误:
// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
if (this.importInterface == null) // <---- Should be "!="
{
Marshal.ReleaseComObject(this.importInterface);
}
this.importInterface = null;
GC.SuppressFinalize(this);
}
使用此“if”语句,MetaDataInfo将泄漏COM对象(如果存在),否则将抛出NullReferenceException。我已在Microsoft Connect上报告了此问题:https://connect.microsoft.com/VisualStudio/feedback/details/779328/。
使用反射器,我找到了MetaDataInfo类的所有用途。(它是一个内部类,因此仅搜索程序集应该是完整的列表。)它仅被使用了一次:public static Guid GetMvid(Assembly assembly)
{
using (MetaDataInfo info = new MetaDataInfo(assembly))
{
return info.Mvid;
}
}
由于所有对 MetaDataInfo 的使用都已经正确地被释放了,这是正在发生的情况:
- 如果 MetaDataInfo.importInterface 不为 null:
- 静态方法 GetMvid 返回 MetaDataInfo.Mvid
using
调用 MetaDataInfo.Dispose- Dispose 泄漏 COM 对象
- Dispose 将 importInterface 设置为 null
- Dispose 调用 GC.SuppressFinalize
- 稍后,当 GC 收集 MetaDataInfo 时,终结器被跳过。
- .
- 如果 MetaDataInfo.importInterface 为 null:
- 静态方法 GetMvid 调用 MetaDataInfo.Mvid 会得到 NullReferenceException。
- 在异常传播之前,
using
调用 MetaDataInfo.Dispose- Dispose 调用 Marshal.ReleaseComObject
- Marshal.ReleaseComObject 抛出 NullReferenceException。
- 因为抛出了异常,Dispose 不会调用 GC.SuppressFinalize
- Dispose 调用 Marshal.ReleaseComObject
- 异常传播到 GetMvid 的调用者。
- 稍后,当 GC 收集 MetaDataInfo 时,它运行终结器
- Finalize 调用 Dispose
- Dispose 调用 Marshal.ReleaseComObject
- Marshal.ReleaseComObject 抛出 NullReferenceException,该异常一直传播到 GC,并终止应用程序。
- Dispose 调用 Marshal.ReleaseComObject
- Finalize 调用 Dispose
顺便说一下,这是来自 MetaDataInfo 的其余相关代码:
public MetaDataInfo(string assemblyName)
{
Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
// The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
Marshal.ReleaseComObject(o);
}
private void InitNameAndMvid()
{
if (this.name == null)
{
uint num;
StringBuilder szName = new StringBuilder {
Capacity = 0
};
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
szName.Capacity = (int) num;
this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
this.name = szName.ToString();
}
}
public Guid Mvid
{
get
{
this.InitNameAndMvid();
return this.mvid;
}
}
编辑2:
我能够重现Microsoft的MetaDataInfo类中的错误。然而,我的复现与我在此处看到的问题略有不同。
- 复现: 我尝试在一个非托管程序集上创建一个MetaDataInfo对象。这将在初始化
importInterface
之前从构造函数抛出异常。 - 我在MSTest中遇到的问题:在某些托管程序集上构造了MetaDataInfo,然后某些事情发生使
importInterface
为null,或在初始化importInterface
之前退出构造函数。- 我知道MetaDataInfo是在托管程序集上创建的,因为MetaDataInfo是一个内部类,唯一调用它的API是通过传递Assembly.Location的结果来进行的。
但是,在Visual Studio中重新创建该问题意味着它为我下载了MetaDataInfo源代码。这是实际的代码,带有原始开发人员的注释。
public void Dispose()
{
// We implement IDisposable on this class because the IMetaDataImport
// can be an expensive object to keep in memory.
if(importInterface == null)
Marshal.ReleaseComObject(importInterface);
importInterface = null;
GC.SuppressFinalize(this);
}
~MetaDataInfo()
{
Dispose();
}
原始代码确认反射器中所看到的问题:if语句是反向的,他们不应该从Finalizer访问托管对象。
之前我说过,因为它从未调用ReleaseComObject,所以它泄漏了COM对象。我在.Net中更多地阅读了有关使用COM对象的信息,如果我理解正确,那是不正确的: COM对象在调用Dispose()时不被释放,但当垃圾收集器开始收集运行时可调用包装器(RCW)时,它会被释放,这是托管对象。即使它是非托管COM对象的包装器,RCW仍然是托管对象,"不要从终结器访问托管对象"的规则仍然适用。