24

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

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");
    }
}

此代码基于CodeProject 文章。我添加调用是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

.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

.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 能够访问,它怎么可能为 null.ClassPath.ClassName呢?

对此有什么想法吗?

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

4

3 回答 3

35

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

这是Dispose()来自的方法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方法,因此new永远不会调用 Dispose。

编辑

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

我找不到此问题的 Microsoft Connect 条目,因此我制作了一个. 如果您可以复制或者这是否影响了您,请随意投票。

于 2012-08-10T06:11:40.413 回答
0

此问题还会导致 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 .

于 2016-07-26T22:34:17.353 回答
0

我们看到了类似的问题,

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

尽管我知道这不是解决方案,但只是解决该错误的方法

于 2018-12-14T12:00:15.927 回答