Подтвердить что ты не робот

Использование предложения не может вызвать Dispose?

Я использую Visual Studio 2010 для целевого профиля клиента .NET 4.0. У меня есть класс С# для обнаружения, когда данный процесс запускается/заканчивается. Для этого класс использует 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(), поскольку он просочился в память: около 32 КБ каждый раз, когда EventArrived поднимается один раз в секунду. Утечка очевидна как для WinXP, так и для Win7 (64-разрядная версия).

Пока все хорошо. Пытаясь быть добросовестным, я добавил предложение try-finally, например:

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

Нет проблем. Еще лучше, предложение С# using более компактно, но эквивалентно:

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

Отлично, только теперь утечка памяти вернулась. Что случилось?

Ну, я не знаю. Но я попытался разобрать две версии с ILDASM, которые почти, но не совсем одинаковы.

IL от 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 от 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?

Любые мысли об этом?

Кроме того, мне интересно, поможет ли это поведение объяснить неразрешенное обсуждение здесь: Утечка памяти в WMI при запросе журналов событий.

4b9b3361

Ответ 1

На первый взгляд появляется ошибка в 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 никогда не вызывается.

ИЗМЕНИТЬ

Обычно я бы не предполагал, что что-то вроде этого ошибка, но поскольку использование new для Dispose обычно является плохой практикой (тем более, что ManagementBaseObject не запечатано), и поскольку существует комментарий без комментариев использование new, я думаю, что это ошибка.

Я не мог найти запись Microsoft Connect для этой проблемы, поэтому я сделал один. Не стесняйтесь повышать, если вы можете воспроизвести или это повлияло на вас.

Ответ 2

Эта проблема также приводит к сбою MS Unit Test Framework и вешает навсегда в конце всех тестов (в Visual Studio 2015, обновление 3). К сожалению, ошибка все еще сохраняется, поскольку я пишу это. В моем случае протекает следующий код:

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 каждый время, которое он висит.

Ответ 3

Мы видим похожую проблему,

Для устранения утечки достаточно вызвать GC.WaitForPendingFinalizers() один раз.

хотя я знаю, что это не решение, а просто обходной путь