Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using clause fails to call Dispose?

Tags:

I'm using Visual Studio 2010 to target .NET 4.0 Client Profile. I have a C# class to detect when a given process starts/terminates. For this the class uses a ManagementEventWatcher, which is initialised as below; query, scope and watcher are class fields:

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(); 

The handler for event EventArrived looks like this:

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

This code is based on a CodeProject article. I added the call to mbo.Dispose() because it leaked memory: about 32 KB every time EventArrived is raised, once per second. The leak is obvious on both WinXP and Win7 (64-bit).

So far so good. Trying to be conscientious I added a try-finally clause, like this:

var mbo = e.NewEvent; try {     eventName = mbo.ClassPath.ClassName; } finally {     mbo.Dispose(); } 

No problem there. Better still, the C# using clause is more compact but equivalent:

using (var mbo = e.NewEvent) {     eventName = mbo.ClassPath.ClassName; } 

Great, only now the memory leak is back. What happened?

Well, I don't know. But I tried disassembling the two versions with ILDASM, which are almost but not quite the same.

IL from 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 

IL from 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 

Apparently the problem is this line:

IL_003c:  brfalse.s  IL_0044 

which is equivalent to if (mbo != null), so mbo.Dispose() is never called. But how is it possible for mbo to be null if it was able to access .ClassPath.ClassName?

Any thoughts on this?

Also, I'm wondering if this behaviour helps explain the unresolved discussion here: Memory leak in WMI when querying event logs.

like image 209
groverboy Avatar asked Aug 10 '12 06:08

groverboy


People also ask

Does using statement Call Dispose?

The using statement guarantees that the object is disposed in the event an exception is thrown. It's the equivalent of calling dispose in a finally block.

How do I call a Dispose method?

The Dispose() methodThe Dispose method performs all object cleanup, so the garbage collector no longer needs to call the objects' Object. Finalize override. Therefore, the call to the SuppressFinalize method prevents the garbage collector from running the finalizer. If the type has no finalizer, the call to GC.


1 Answers

At first glance, there appears to be a bug in ManagementBaseObject.

Here's the Dispose() method from ManagementBaseObject:

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

Notice that it is declared as new. Also notice that when the using statement calls Dispose, it does so with the explicit interface implementation. Thus the parent Component.Dispose() method is called, and _wbemObject.Dispose() is never called. ManagementBaseObject.Dispose() should not be declared as new here. Don't believe me? Here's a comment from Component.cs, right above it's Dispose(bool) method:

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

Since here the using statement calls the explicit IDisposable.Dispose method, the new Dispose never gets called.

EDIT

Normally I would not assume that something like this a bug, but since using new for Dispose is usually bad practice (especially since ManagementBaseObject is not sealed), and since there is no comment explaining the use of new, I think this is a bug.

I could not find a Microsoft Connect entry for this issue, so I made one. Feel free to upvote if you can reproduce or if this has affected you.

like image 80
Michael Graczyk Avatar answered Sep 24 '22 05:09

Michael Graczyk