La clause Using échoue à appeler Dispose?

J'utilise Visual Studio 2010 pour cibler le profil Client. Net 4.0. J'ai une Classe C# pour détecter quand un processus donné commence / se termine. Pour cela, la classe utilise un ManagementEventWatcher, qui est initialisé comme ci-dessous; query, scope et watcher sont des champs de classe:

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(@"\.rootCIMV2");

watcher = new ManagementEventWatcher(scope, query);
watcher.EventArrived += WatcherEventArrived;
watcher.Start();

Le gestionnaire de l'événement EventArrived ressemble à ceci:

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

Ce code est basé sur un article CodeProject . J'ai ajouté l'appel à mbo.Dispose() car il a fui la mémoire: environ 32 KB à chaque fois EventArrived est déclenché, une fois par seconde. La fuite est évidente sur WinXP et Win7 (64 bits).

Jusqu'à présent tout va bien. En essayant d'être consciencieux, j'ai ajouté une clause try-finally, comme ceci:

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

Pas de problème. Mieux encore, la clause C # using est plus compacte mais équivalente:

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

Génial, seulement maintenant la fuite de mémoire est de retour. Qu'est-ce qui s'est passé?

Eh bien, Je ne sais pas. Mais j'ai essayé de démonter les deux versions avec ILDASM, qui sont presque mais pas tout à fait le de même.

Il de 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 de 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

Apparemment, le problème est cette ligne:

IL_003c:  brfalse.s  IL_0044

, Qui est équivalent à if (mbo != null), donc mbo.Dispose() n'est jamais appelée. Mais comment est-il possible que mbo soit null s'il a pu accéder à .ClassPath.ClassName?

Des idées à ce sujet?

En outre, je me demande si ce comportement aide à expliquer la discussion non résolue ici: fuite de mémoire dans WMI lors de l'interrogation des journaux d'événements.

24
demandé sur abatishchev 2012-08-10 10:01:54

2 réponses

, À première vue, il semble y avoir un bug dans ManagementBaseObject.

Voici la méthode Dispose() de ManagementBaseObject:

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

Notez qu'il est déclaré comme new. Notez également que lorsque l'instruction using appelle Dispose, elle le fait avec l'implémentation explicite de l'interface. Ainsi, la méthode parent Component.Dispose() est appelée, et _wbemObject.Dispose() n'est jamais appelée. ManagementBaseObject.Dispose() ne doit pas être déclaré comme new ici. Ne me croyez pas? Voici un commentaire de Component.cs, juste au-dessus de Dispose(bool) méthode:

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

Comme ici l'instruction using appelle la méthode IDisposable.Dispose explicite, la new Dispose n'est jamais appelée.

Modifier

Normalement je n'aurais pas supposer que quelque chose comme ça un bug, mais depuis l'utilisation de new pour Dispose est généralement de mauvaise pratique (surtout depuis que ManagementBaseObject n'est pas étanche), et depuis il est aucun commentaire expliquant l'utilisation de new, je pense que c'est un bug.

Je n'ai pas trouvé D'entrée Microsoft Connect pour ce problème, donc j'en ai fait un . N'hésitez pas à upvote si vous pouvez reproduire ou si cela vous a affecté.

34
répondu Michael Graczyk 2012-08-12 09:09:25

Ce problème provoque également L'échec de MS Unit Test Framework et le blocage définitif à la fin de l'exécution de tous les tests (sous Visual Studio 2015, mise à jour 3). Malheureusement le bug persiste alors que j'écris cela. Dans mon cas, le code suivant fuit:

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

Et quel Framework de Test se plaint d'un thread qui n'est pas arrêté:

Système.AppDomainUnloadedException: tentative d'accès à un AppDomain déchargé. cela peut se produire si le(s) test (s) a thread mais ne l'a pas arrêté . Assurez-vous que tous les threads démarrés par le(s) test (s) sont arrêtés avant la fin.

Et j'ai réussi à le contourner en exécutant le code dans un autre thread (par conséquent, après la sortie du thread de démarrage, espérons que tous les autres threads générés sont fermés et les ressources sont libérées de manière appropriée):

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

Je ne préconise pas que ce soit la solution au problème (en fait, frayer un thread juste pour cet appel est un idée horrible), mais au moins je peux exécuter des tests à nouveau sans avoir besoin de redémarrer Visual Studio chaque fois qu'il se bloque.

0
répondu Sepehr 2016-07-26 22:34:17