using 子句调用 Dispose 失败?

Posted

技术标签:

【中文标题】using 子句调用 Dispose 失败?【英文标题】:Using clause fails to call Dispose? 【发布时间】:2012-08-07 10:30:46 【问题描述】:

我正在使用 Visual Studio 2010 来定位 .NET 4.0 客户端配置文件。我有一个 C# 类来检测给定进程何时开始/终止。为此,该类使用一个 ManagementEventWatcher,其初始化如下; queryscopewatcher 是类字段:

query = new WqlEventQuery();
query.EventClassName = "__InstanceOperationEvent";
query.WithinInterval = new TimeSpan(0, 0, 1);
query.Condition = "TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'notepad.exe'";

scope = new ManagementScope(@"\\.\root\CIMV2");

watcher = new ManagementEventWatcher(scope, query);
watcher.EventArrived += WatcherEventArrived;
watcher.Start();

EventArrived 事件的处理程序如下所示:

private void WatcherEventArrived(object sender, EventArrivedEventArgs e)

    string eventName;

    var mbo = e.NewEvent;
    eventName = mbo.ClassPath.ClassName;
    mbo.Dispose();

    if (eventName.CompareTo("__InstanceCreationEvent") == 0)
    
        Console.WriteLine("Started");
    
    else if (eventName.CompareTo("__InstanceDeletionEvent") == 0)
    
        Console.WriteLine("Terminated");
    

此代码基于a CodeProject article。我添加了对mbo.Dispose() 的调用,因为它泄漏了内存:每次引发 EventArrived 时大约 32 KB,每秒一次。泄漏在 WinXP 和 Win7(64 位)上都很明显。

到目前为止一切顺利。为了尽心尽责,我添加了一个try-finally 子句,如下所示:

var mbo = e.NewEvent;
try

    eventName = mbo.ClassPath.ClassName;

finally

    mbo.Dispose();

那里没问题。更好的是,C# using 子句更紧凑但等效:

using (var mbo = e.NewEvent)

    eventName = mbo.ClassPath.ClassName;

太好了,只是现在内存泄漏又回来了。发生了什么?

嗯,我不知道。但是我尝试用ILDASM反汇编这两个版本,它们几乎但不完全相同。

来自try-finally的IL:

.try

  IL_0030:  nop
  IL_0031:  ldloc.s    mbo
  IL_0033:  callvirt   instance class [System.Management]System.Management.ManagementPath [System.Management]System.Management.ManagementBaseObject::get_ClassPath()
  IL_0038:  callvirt   instance string [System.Management]System.Management.ManagementPath::get_ClassName()
  IL_003d:  stloc.3
  IL_003e:  nop
  IL_003f:  leave.s    IL_004f
  // end .try
finally

  IL_0041:  nop
  IL_0042:  ldloc.s    mbo
  IL_0044:  callvirt   instance void [System.Management]System.Management.ManagementBaseObject::Dispose()
  IL_0049:  nop
  IL_004a:  ldnull
  IL_004b:  stloc.s    mbo
  IL_004d:  nop
  IL_004e:  endfinally
  // end handler
IL_004f:  nop

来自using的IL:

.try

  IL_002d:  ldloc.2
  IL_002e:  callvirt   instance class [System.Management]System.Management.ManagementPath [System.Management]System.Management.ManagementBaseObject::get_ClassPath()
  IL_0033:  callvirt   instance string [System.Management]System.Management.ManagementPath::get_ClassName()
  IL_0038:  stloc.1
  IL_0039:  leave.s    IL_0045
  // end .try
finally

  IL_003b:  ldloc.2
  IL_003c:  brfalse.s  IL_0044
  IL_003e:  ldloc.2
  IL_003f:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0044:  endfinally
  // end handler
IL_0045:  ldloc.1

显然问题出在这一行:

IL_003c:  brfalse.s  IL_0044

相当于if (mbo != null),所以永远不会调用mbo.Dispose()。但是如果可以访问.ClassPath.ClassName,mbo 怎么可能为空呢?

对此有什么想法吗?

另外,我想知道这种行为是否有助于解释此处未解决的讨论:Memory leak in WMI when querying event logs。

【问题讨论】:

我强烈怀疑您误诊了这一点。我从未见过using 语句失败。请注意,您的 try/finally 版本当前无法编译,因此这显然不是您的真实代码。您能否发布一个简短但完整的程序来演示该问题? @JonSkeet 你是对的,现在尝试终于修复了。 @groverboy 没关系,但从 IL 看来,您的 try/finally 代码也将 mbo 设置为 null,除非它只是 Debug 构建自动执行此操作。 . @MichaelGraczyk 不,你是对的,原来的 try-finally 包含了mbo = null,我认为这是多余的。 @groverboy 我在我编辑的链接上提交了一个连接条目。 【参考方案1】:

乍一看,ManagementBaseObject 中似乎存在错误。

这是来自ManagementBaseObjectDispose() 方法:

    public new void Dispose() 
    
        if (_wbemObject != null) 
        
            _wbemObject.Dispose();
            _wbemObject = null;
         
        base.Dispose();
        GC.SuppressFinalize(this); 
     

注意它被声明为new。另请注意,当using 语句调用Dispose 时,它是通过显式接口实现来实现的。因此,父 Component.Dispose() 方法被调用,而 _wbemObject.Dispose() 永远不会被调用。 ManagementBaseObject.Dispose() 不应在此处声明为 new。不相信我?这是来自Component.cs 的评论,正上方是Dispose(bool) 方法:

    ///    <para>
    ///    For base classes, you should never override the Finalier (~Class in C#) 
    ///    or the Dispose method that takes no arguments, rather you should 
    ///    always override the Dispose method that takes a bool.
    ///    </para> 
    ///    <code>
    ///    protected override void Dispose(bool disposing) 
    ///        if (disposing) 
    ///            if (myobject != null)  
    ///                myobject.Dispose();
    ///                myobject = null; 
    ///             
    ///        
    ///        if (myhandle != IntPtr.Zero)  
    ///            NativeMethods.Release(myhandle);
    ///            myhandle = IntPtr.Zero;
    ///        
    ///        base.Dispose(disposing); 
    ///    

因为这里using 语句调用显式IDisposable.Dispose 方法,所以new Dispose 永远不会被调用。

编辑

通常我不会认为这是一个错误,但因为使用new 来表示Dispose 通常是不好的做法(特别是因为ManagementBaseObject 没有密封),并且因为有 没有评论解释new的使用,我认为这是一个bug。

我找不到此问题的 Microsoft Connect 条目so I made one。如果您可以复制或者这是否影响了您,请随意投票。

【讨论】:

不错。我确实想知道像显式接口实现这样的东西在这里是否相关,但没有检查细节。 上周我遇到了同样的问题。问题是用new 显式调用IDisposable.Dispose 它看起来确实像一个错误,尤其是当没有关于 new 或该方法的 XML 文档的评论时。 @AMissico 是的,我为它提交了一个连接条目。在编辑中添加了一个链接。 5 年后。 .NET 4.7。并且错误仍然存​​在ManagementBaseObject.Dispose【参考方案2】:

此问题还会导致 MS 单元测试框架在运行所有测试结束时失败并永远挂起(在 Visual Studio 2015 下,更新 3)。不幸的是,在我写这篇文章时,这个错误仍然存​​在。在我的情况下,以下代码正在泄漏:

using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))

    ....

测试框架抱怨的是线程没有被关闭:

System.AppDomainUnloadedException:试图访问已卸载的 AppDomain。 如果测试启动了一个线程但没有停止它,就会发生这种情况。确保测试启动的所有线程在完成之前都已停止。

我设法通过在另一个线程中执行代码来绕过它(因此,在启动线程退出后,希望其中产生的所有其他线程都已关闭并适当地释放资源):

Thread searcherThread = new Thread(new ThreadStart(() =>

    using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
    
        ....
    
));
searcherThread.Start();
searcherThread.Join();

我并不主张这是解决问题的方法(事实上,只为这个调用生成一个线程是一个可怕的想法),但至少我可以再次运行测试而无需每次都重新启动 Visual Studio挂起的时间。

【讨论】:

您是否能够通过使用try-finally 而不是using(并产生一个新线程)来绕过它? @groverboy我不明白为什么这不起作用,在我自己的代码中,我使用了 using 块,因为该问题与未调用 Dispose 无关。它被调用,但由于一个错误,它不会传播到基类,因此基类中的资源会泄漏。【参考方案3】:

我们看到了类似的问题,

调用 GC.WaitForPendingFinalizers() 一次就足以修复泄漏

虽然我知道这不是一个解决方案,只是一个解决问题的方法

【讨论】:

GC.WaitForPendingFinalizers 暂停调用线程,直到所有标记为终结的对象都调用了它们的 Finalize 方法。这似乎可能会影响应用程序的性能,因此最好尽可能避免。您是否能够通过使用 try-finally 而不是 using 子句来修复泄漏? 一位同事通过查看 .NET 代码正确地修复了它,找出它泄漏 COM 对象的位置,然后适当地处理它们。

以上是关于using 子句调用 Dispose 失败?的主要内容,如果未能解决你的问题,请参考以下文章

using (SqlConnection connection = new SqlConnection(connectionString))

c# using的疑问

Dispose() 怎么知道它是因为异常而被调用的?

利用using和try/finally语句来清理资源

处理 WebSocket 的类 close 和 dispose (Observable.Using)

提高系统性能