20

(我知道,这是一个非常长的问题。到目前为止,我试图将这个问题与我的调查分开,所以它更容易阅读。)

我正在使用 MSTest.exe 运行我的单元测试。有时,我会看到此测试错误:

关于单个单元测试方法:“在测试运行时代理进程已停止。”

在整个测试运行中:

后台线程之一引发异常:
System.NullReferenceException:对象引用未设置为对象的实例。
   在 System.Runtime.InteropServices.Marshal.ReleaseComObject(对象 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 目录中的 System.Management 中找到了 MetaDataInfo 类。(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 不为空:
    • 静态方法 GetMvid 返回 MetaDataInfo.Mvid
    • using调用 MetaDataInfo.Dispose
      • Dispose 泄漏 COM 对象
      • Dispose 将 importInterface 设置为 null
      • Dispose 调用 GC.SuppressFinalize
    • 稍后,当 GC 收集 MetaDataInfo 时,会跳过终结器。
  • .
  • 如果 MetaDataInfo.importInterface 为空:
    • 静态方法 GetMvid 获取调用 MetaDataInfo.Mvid 的 NullReferenceException。
    • 在异常向上传播之前,using调用 MetaDataInfo.Dispose
      • Dispose 调用 Marshal.ReleaseComObject
        • Marshal.ReleaseComObject 引发 NullReferenceException。
      • 因为抛出异常,Dispose 不会调用 GC.SuppressFinalize
    • 异常传播到 GetMvid 的调用者。
    • 稍后,当 GC 收集到 MetaDataInfo 时,它会运行 Finalizer
      • 完成调用 Dispose
        • Dispose 调用 Marshal.ReleaseComObject
          • Marshal.ReleaseComObject 抛出一个 NullReferenceException,它一直传播到 GC,应用程序被终止。

对于它的价值,这里是来自 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 是在某个托管程序集上构造的,并且在初始化 之前发生了一些事情使importInterfacenull 或退出构造函数。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 语句是向后的,它们不应该从终结器访问托管对象。

我之前说过,因为它从来没有调用ReleaseComObject,它正在泄漏 COM 对象。我阅读了更多关于 .Net 中使用 COM 对象的信息,如果我理解正确,那是不正确的:调用 Dispose() 时 COM 对象没有被释放,但是当垃圾收集器到达时它被释放收集运行时可调用包装器,它是一个托管对象。尽管它是一个非托管 COM 对象的包装器,但 RCW 仍然是一个托管对象,并且关于“不要从终结器访问托管对象”的规则仍然应该适用。

4

3 回答 3

1

尝试将以下代码添加到您的类定义中:

bool _disposing = false  // class property

public void Dispose()
{
    if( !disposing ) 
        Marshal.ReleaseComObject(importInterface);
    importInterface = null; 
    GC.SuppressFinalize(this);

    disposing = true;
}
于 2013-02-25T10:19:03.920 回答
0

如果 MetaDataInfo 使用 IDisposable 模式,那么还应该有一个终结器(C# 中的 ~MetaDataInfo())。using 语句将确保调用 Dispose(),它将 importInterface 设置为 null。然后当 GC 准备完成时,调用 ~MetaDataInfo(),它通常会调用 Dispose(或者更确切地说,重载采用 bool disposing:Dispose(false))。

我会说这个错误应该经常出现。

于 2013-02-22T20:30:35.807 回答
0

您是否尝试为您的测试解决此问题?如果是这样,请重写您的使用。不要自己处理它,而是编写一些代码来使用反射来访问私有字段并正确处理它们,然后调用 GC.SuppressFinalize 以防止终结器运行。

作为一个简短的旁白(喜欢你的调查顺便说一句)你说 Dispose 调用 Finalize。反之亦然,当 GC 调用 Dispose 时,Finalize 被调用。

于 2013-02-23T00:10:33.007 回答