Использование предложения не может вызвать 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 при запросе журналов событий.
Ответы
Ответ 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() один раз.
хотя я знаю, что это не решение, а просто обходной путь