C# Using子句调用Dispose失败?

C# Using子句调用Dispose失败?,c#,.net,wmi,using,C#,.net,Wmi,Using,我正在使用VisualStudio2010以.NET4.0客户端配置文件为目标。我有一个C#类来检测给定进程何时启动/终止。为此,该类使用ManagementEventWatcher,其初始化如下查询、范围和观察者是类字段: query = new WqlEventQuery(); query.EventClassName = "__InstanceOperationEvent"; query.WithinInterval = new TimeSpan(0, 0, 1); query.Condi

我正在使用VisualStudio2010以.NET4.0客户端配置文件为目标。我有一个C#类来检测给定进程何时启动/终止。为此,该类使用ManagementEventWatcher,其初始化如下<代码>查询、
范围
观察者
是类字段:

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");
    }
}
var mbo = e.NewEvent;
try
{
    eventName = mbo.ClassPath.ClassName;
}
finally
{
    mbo.Dispose();
}
此代码基于。我添加了对
mbo.Dispose()
的调用,因为它泄漏了内存:每次引发eventArrival时大约32KB,每秒一次。漏洞在WinXP和Win7(64位)上都很明显

到目前为止还不错。为了尽责,我添加了一个
try finally
子句,如下所示:

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");
    }
}
var mbo = e.NewEvent;
try
{
    eventName = mbo.ClassPath.ClassName;
}
finally
{
    mbo.Dispose();
}
没问题。更好的是,C#
using
子句更紧凑,但相当于:

using (var mbo = e.NewEvent)
{
    eventName = mbo.ClassPath.ClassName;
}
很好,只是现在内存泄漏又回来了。发生了什么事

嗯,我不知道。但我尝试用ILDASM分解这两个版本,它们几乎相同,但并不完全相同

来自
的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
使用

.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()
。但是,如果mbo能够访问
.ClassPath.ClassName
,它怎么可能为空呢

有什么想法吗


另外,我想知道这种行为是否有助于解释此处未解决的讨论:。

乍一看,ManagementBaseObject中似乎有一个bug

以下是
ManagementBaseObject
中的
Dispose()
方法:

    public new void Dispose() 
    {
        if (_wbemObject != null) 
        {
            _wbemObject.Dispose();
            _wbemObject = null;
        } 
        base.Dispose();
        GC.SuppressFinalize(this); 
    } 
请注意,它被声明为
new
。还要注意,当
using
语句调用
Dispose
时,它使用显式接口实现来实现。因此,将调用父级
组件.Dispose()
方法,并且永远不会调用
\u wbeObject.Dispose()
<代码>ManagementBaseObject.Dispose()
不应在此处声明为新的。不相信我?下面是
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

编辑

通常我不会认为这样的东西是一个bug,但由于使用
new
进行
Dispose
通常是不好的做法(特别是
ManagementBaseObject
没有密封),而且没有注释解释
new
的使用,我认为这是一个bug


我找不到此问题的Microsoft Connect条目。如果你能复制或者这影响了你,请随意投票

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

using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
    ....
}
测试框架抱怨的是线程没有被关闭:

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

通过在另一个线程中执行代码,我成功地绕过了它(因此,在启动线程退出后,希望在其中生成的所有其他线程都被关闭,并且相应地释放了资源):


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

我们看到了类似的问题

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


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

我强烈怀疑您误诊了。我从未见过
使用
语句失败。请注意,您的
try/finally
版本当前不会编译,因此这显然不是您真正的代码。你能发布一个简短但完整的程序来演示这个问题吗?@JonSkeet你是对的,现在试试finally fixed。@groverboy这并不重要,但从IL来看,你的
try/finally
代码似乎也将
mbo
设置为
null
,除非只是调试版本自动执行此操作…@MichaelGraczyk不,你是对的,最初的尝试最终包括
mbo=null
,我认为这是多余的。@groverboy我在我的edit.Nice中的链接上提交了一个connect条目。我确实想知道像显式接口实现这样的东西在这里是否相关,但没有检查细节。上周我遇到了同样的问题。这是对
IDisposable.Dispose
new
的显式调用,这就是问题所在。它看起来确实像一个bug,特别是当没有关于
new
的注释或方法的XML文档时。@AMissico是的,我为它提交了一个连接条目。在编辑中添加了一个链接。5年后。净额4.7。这个bug仍然存在,您是否能够通过使用
try finally
而不是
使用
来绕过它(并生成一个新线程)?@groverboyI不明白为什么这样做不起作用,在我自己的代码中,我使用
使用
块,因为这个问题与
Dispose
没有被调用无关。它被调用,但由于一个bug,它不会传播到基类,因此基类中的资源会泄漏。
GC.WaitForPendingFinalizers
暂停调用线程,直到所有标记为终结的对象都调用了它们的
Finalize
方法。这似乎可能会影响应用程序性能,因此最好尽可能避免。是