Using子句无法调用Dispose?
我正在使用Visual Studio 2010来定位.NET 4.0 Client Profile。 我有一个C#类来检测给定进程何时启动/终止。 为此,该类使用ManagementEventWatcher,初始化如下; query
, scope
和watcher
是类字段:
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
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
IL 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能够访问.ClassPath.ClassName
那么mbo怎么可能为null?
有什么想法吗?
另外,我想知道这种行为是否有助于解释这里未解决的讨论: 查询事件日志时WMI中的内存泄漏 。
乍一看, ManagementBaseObject
似乎存在一个错误。
这是ManagementBaseObject
的Dispose()
方法:
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)
方法之上:
///
/// 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. /// ////// 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 Unit Test Framework失败并在运行所有测试结束时永久挂起(在Visual Studio 2015,更新3下)。 不幸的是,当我写这篇文章时,这个bug仍然存在。 在我的情况下,以下代码泄漏:
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query)) { .... }
Test Framework抱怨的是一个线程没有被关闭:
System.AppDomainUnloadedException:尝试访问已卸载的AppDomain。 如果测试开始一个线程但没有停止它,就会发生这种情况 。 确保测试启动的所有线程在完成之前停止。
我设法通过在另一个线程中执行代码来绕过它(因此,在启动程序线程退出后, 希望在其中生成的所有其他线程都关闭并且资源被适当地释放):
Thread searcherThread = new Thread(new ThreadStart(() => { using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query)) { .... } })); searcherThread.Start(); searcherThread.Join();
我并不是说这是问题的解决方案(事实上,为这个调用产生一个线程是一个可怕的想法),但至少我可以再次运行测试而无需每次挂起时重新启动Visual Studio 。