使用子句无法调用Dispose?

时间:2012-08-10 06:01:55

标签: c# .net wmi using

我正在使用Visual Studio 2010来定位.NET 4.0 Client Profile。我有一个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()。但是,如果mbo能够访问.ClassPath.ClassName

,那么mbo怎么可能为null?

对此有何想法?

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

3 个答案:

答案 0 :(得分:34)

乍一看,ManagementBaseObject似乎有一个错误。

以下是来自Dispose()的{​​{1}}方法:

ManagementBaseObject

请注意,它被声明为 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 Dispose永远不会被调用。

修改

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

我无法找到此问题的Microsoft Connect条目so I made one。如果您可以复制或者这对您有影响,请随意提升。

答案 1 :(得分:0)

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

using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
    ....
}

Test Framework抱怨的是一个线程没有被关闭:

  

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

我设法通过在另一个线程中执行代码来解决它(因此,在启动程序线程退出后,希望在其中生成的所有其他线程都关闭并且资源被适当地释放):< / p>

Thread searcherThread = new Thread(new ThreadStart(() =>
{
    using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
    {
        ....
    }
}));
searcherThread.Start();
searcherThread.Join();

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

答案 2 :(得分:0)

我们看到了类似的问题,

调用一次 GC.WaitForPendingFinalizers()足以解决泄漏

尽管我知道这不是一个解决方案,但只能解决该错误