Удаление моего System.IDisposable объекта в моем финализаторе
Здесь есть несколько обсуждений о Stackru о том, что делать, если мой объект управляет другими управляемыми объектами, которые реализуют System.IDisposable
,
Примечание: ниже я не говорю о неуправляемом коде. Я полностью понимаю важность очистки неуправляемого кода
В большинстве обсуждений говорится, что если вашему объекту принадлежит другой управляемый объект, который реализует System.IDisposable
, то вы должны также реализовать System.IDisposable
, в этом случае вы должны позвонить Dispose()
из одноразовых предметов, которые держит ваш предмет. Это было бы логично, потому что вы не знаете, использует ли принадлежащий вам одноразовый объект неуправляемый код. Вы только знаете, что создатель другого объекта думал, что было бы разумно, если бы вы позвонили Dispose
как только вам больше не нужен объект.
Очень хорошее объяснение шаблона Disposable было дано здесь на Stackru, отредактированном вики сообщества:
Правильное использование интерфейса IDisposable
Довольно часто, а также в упомянутой ссылке я читаю:
"Вы не знаете порядок, в котором два объекта уничтожены. Вполне возможно, что в вашем
Dispose()
код, управляемого объекта, от которого вы пытаетесь избавиться, больше нет ".
Это сбивает с толку меня, потому что я думал, что, пока любой объект содержит ссылку на объект X, то объект X не будет и не может быть завершен.
Или другими словами: пока мой объект содержит ссылку на объект X I, можно быть уверенным, что объект X не завершен.
Если это правда, то почему это может быть так, что если я удерживаю ссылку на свой объект до завершения, то объект, на который я ссылаюсь, уже завершен?
5 ответов
Истина где-то между двумя:
- Объект не может быть собран мусором, поэтому вероятность того, что объект больше не будет "там", не соответствует действительности.
- Объект может быть завершен, когда на него больше нет ссылок из других не финализируемых объектов.
Если объект X ссылается на объект Y, но оба они являются финализуемыми, тогда вполне возможно, что объект Y будет завершен до объекта X или даже для их одновременной финализации.
Если ваше предположение было верным, то вы могли бы создать два объекта, которые ссылаются друг на друга (и имеют финализаторы), и они никогда не могли бы быть собраны сборщиком мусора, потому что они никогда не могли быть завершены.
Цитируя Эрика Липперта, " Когда все, что вы знаете, неправильно, часть вторая
Миф: сохранение ссылки на объект в переменной препятствует запуску финализатора, пока переменная жива; локальная переменная всегда жива, по крайней мере, до тех пор, пока управление не покинет блок, в котором была объявлена локальная переменная.
{ Foo foo = new Foo(); Blah(foo); // Last read of foo Bar(); // We require that foo not be finalized before Bar(); // Since foo is in scope until the end of the block, // it will not be finalized until this point, right? }
Спецификация C# гласит, что среде выполнения разрешена широкая широта для обнаружения, когда к хранилищу, содержащему ссылку, больше никогда не будет доступа, и прекращение обработки этого хранилища как корня сборщика мусора. Например, предположим, у нас есть локальная переменная
foo
и ссылка записана в него в верхней части блока. Если дрожание знает, что конкретное чтение является последним чтением этой переменной, переменная может быть юридически удалена из набора корней GC немедленно; ему не нужно ждать, пока управление покинет область действия переменной. Если эта переменная содержала последнюю ссылку, GC может обнаружить, что объект недоступен и немедленно поместить его в очередь финализатора. использованиеGC.KeepAlive
чтобы избежать этого.Почему у джиттера такая широта? Предположим, что локальная переменная зарегистрирована в регистре, необходимом для передачи значения
Blah()
, Еслиfoo
находится в реестре, которыйBar()
необходимо использовать, нет смысла сохранять значение "никогда больше не будет читаться"foo
в стеке доBar()
называется. (Если вам интересны реальные детали кода, сгенерированного джиттером, см. Более глубокий анализ этой проблемы Рэймондом Ченом.)Дополнительное бонусное удовольствие: среда выполнения использует менее агрессивную генерацию кода и менее агрессивную сборку мусора при запуске программы в отладчике, потому что плохой опыт отладки состоит в том, что отлаживаемые объекты внезапно исчезают, даже если переменная, ссылающаяся на объект, находится в объем. Это означает, что если у вас есть ошибка, когда объект завершается слишком рано, вы, вероятно, не сможете воспроизвести эту ошибку в отладчике!
Смотрите последний пункт в этой статье для еще более ужасной версии этой проблемы.
После всех ответов я создал небольшую программу, которая показывает, что написал Джодрелл (спасибо, Джодрелл!)
- Объект можно собирать мусором, как только он не будет использоваться, даже если у меня есть ссылка на него
- Это будет сделано только если не отладка.
Я написал простой класс, который выделяет неуправляемую память и MemoryStream. Последний реализует System.IDisposable.
По мнению всех в Stackru, я должен реализовать System.IDisposable и освободить неуправляемую память, а также Dispose управляемого memoryStream, если вызывается мой Dispose, но если вызывается мой финализатор, я должен только освобождать неуправляемую память.
Я пишу некоторые диагностические сообщения консоли
class ClassA : System.IDisposable
{
IntPtr memPtr = Marshal.AllocHGlobal(1024);
Stream memStream = new MemoryStream(1024);
public ClassA()
{
Console.WriteLine("Construct Class A");
}
~ClassA()
{
Console.WriteLine("Finalize Class A");
this.Dispose(false);
}
public void Dispose()
{
Console.WriteLine("Dispose()");
this.Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing)
{
Console.WriteLine("Dispose({0})", disposing.ToString());
if (!this.IsDisposed)
{
if (disposing)
{
Console.WriteLine("Dispose managed objects");
memStream.Dispose();
}
Console.WriteLine("Dispose unmanaged objects");
Marshal.FreeHGlobal(memPtr);
}
}
public bool IsDisposed { get { return this.memPtr == null; } }
}
Эта программа следует шаблону утилизации, как описано много раз, здесь в стеке потока при правильном использовании интерфейса IDisposable
кстати: для простоты я исключил обработку исключений
Простая консольная программа создает объект, не использует его, но сохраняет ссылку на него и заставляет сборщик мусора собирать:
private static void TestFinalize()
{
ClassA a = new ClassA() { X = 4 };
Console.WriteLine("Start Garbage Collector");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done");
}
Обратите внимание, что переменная a содержит ссылку на объект до конца процедуры. Я забыл утилизировать, поэтому мой финализатор должен позаботиться об утилизации
Вызовите этот метод из вашего основного. Запустите сборку (выпуска) из отладчика и запустите ее из командной строки.
- При запуске из отладчика объект остается живым до конца процедуры, то есть до тех пор, пока сборщик мусора не завершит сбор
- Если запустить из командной строки, то объект завершается до завершения процедуры, хотя у меня все еще есть ссылка на объект.
Так что Джодрелл прав:
неуправляемый код требует Dispose() и Finalize, используйте Dispose(bool)
Управляемые одноразовые объекты требуют Dispose(), предпочтительно через Dispose(bool). В Dispose(bool) вызывать только Dispose() управляемых объектов при утилизации
не доверяйте отладчику: это делает объекты завершенными в разные моменты, чем без отладчика
Если все сделано правильно, вам не нужно беспокоиться об утилизации предметов, которые уже утилизированы. Каждая реализация Dispose
просто не надо ничего делать, если он был утилизирован раньше.
Таким образом, вы действительно не можете знать, были ли удалены или завершены какие-либо дочерние объекты (поскольку порядок завершения является случайным, см. Другие публикации), но вы все равно можете безопасно вызывать их метод Dispose.
В большинстве случаев, когда Finalize
вызывается для объекта, который содержит ссылки на один или несколько IDisposable
объекты, один или несколько из следующих будут применяться:
- Другой объект уже был очищен, и в этом случае вызывается
Dispose
в лучшем случае бесполезно. - Другой объект планируется завершить как можно скорее, но пока нет, и в этом случае вызов
Dispose
вероятно, не нужно. - Код очистки другого объекта нельзя безопасно использовать в контексте потоков финализатора, в этом случае вызов
Dispose
может быть катастрофическим. - Другой объект все еще используется кодом в другом месте, и в этом случае вызов
Dispose
может быть катастрофическим.
Есть несколько ситуаций, когда код знает достаточно о IDisposable
объекты, с которыми он имеет дело, знают, что ни одно из вышеперечисленного не применимо, либо что оно должно вызывать очистку, несмотря на вышесказанное; эти ситуации, однако, могут лучше обслуживаться, если другие объекты предоставляют метод, отличный отDispose
который может вызывать завершаемый объект.