Pull to refresh

Comments 65

GC.WaitForPendingFinalizers() у вас не срабатывает, потому что сам объект еще не собран сборщиком мусора.

Правильный код может быть таким:

temporaryFile = null;
GC.Collect();
GC.WaitForPendingFinalizers();

точно: я забыл написать
temporaryFile = null;

рука отказалось такое писать :).

да, в таком случае этот способо гораздо более прост.
кстати, ведь не советуют вызывать GC.Collect(); или GC.WaitForPendingFinalizers();
утверждают, что это все равно только просьба к сборщику мусора, а когда он на самом деле захочет делать работу неизвестно.
или запутывают?
Это не просьба, а повеление.
Другое дело, что объекты, на которые есть ссылки, не будут собраны, но думаю, это и так понятно.
По поводу не вызывать — действительно, лучше не вызывать в реальном коде. Но мы ведем речь о тестах, здесь немного более широкие рамки допустимого
Убедили. Так что теперь у меня есть два способа тестировать финализатор.
Спасибо!
И это не панацея. Сборщик мусора может решить, что чистить память совершенно необязательно. Чтобы отучить его думать самостоятельно, надо вызвать перегруженный метод Collect с GCCollectionMode.Forced.
Более того, даже подобный вызов у меня в 1 случае из 100, наверное, не помогает.
Смотрим описание метода:
Collect() Forces an immediate garbage collection of all generations

Это то же самое, что и с флагом GCCollectionMode.Forced ( по крайней мере, в текущих версиях CLR)

То, что вызов не срабатывает — плохо, ищите проблему в своем коде.
Финализаторы в любом случае должны отработать по завершению программы

Кстати, это только в DEBUG режиме нужно null присваивать, или в RELEASE тоже?
Ведь если нет ссылки на объект, то вроде бы зачем присваивать null?
В любом режиме нужно, если вы хотите гарантировать, что ссылки на объект больше нет.
Область видимости переменной temporaryFile — до конца метода, поэтому сохраненная в нем ссылка может существовать на любом промежутке от последнего присваивания до закрывающей фигурной скобки.
Чтоб гарантировать, что ссылки больше нет, необходимо присвоить null или другое значение, таким образом потеряется старое, и сборщик мусора будет вправе удалить такой объект
А как же все эти трюки, что таймер (созданный в main) запущенный в DEBUG режиме работает до конца программы, но в RELEASE режиме умирает довольно быстро?
Это именно трюки, т.к. точное время сборки объекта никто не гарантирует.
Как я уже сказал, ссылка будет потеряна не раньше, чем ее последнее использование. В релизе производятся более агрессивные оптимизации, поэтому ссылка может быть потеряна сразу же (но гарантии никакой нет).
Если вы хотите продлить жизнь ссылки, посмотрите на GC.KeepAlive()
При компиляции с параметром debug+ (или наличии атрибута сборки System.Diagnostics.DebuggableAttribute) JIT считает что время жизни локальной переменной — до завершения метода. В противном случае, JIT оптимизирует код, а GC, не найдя ссылок на переменную, выполнит финализацию.
В первый вызов GC.Collect() будут собраны только те объекты, у которых нет финализатора. Затем во втором вызове вы принуждаете финализаторы отработать. И вот тут нужно еще раз вызвать GC.Collect(), чтобы сборщик собрал объекты, финализаторы которых отработали. Итого:

temporaryFile = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Последняя строчка не нужна — наша задача не собрать всю память, а запустить финализатор.
Ох да, вы правы. Не подумал.
и кстати, совет с исключением в финализаторе хороший, годный, по крайней мере в дебаге (в релизе лучше все-таки тихо вызвать Dispose)
кажется, встречал подобный совет еще у Рихтера
В финализаторе исключения игнорируются.
Совет был писать нечто вроде Debug.WriteLine(«AAA!»)…
Этот совет подходит не для всех объектов и не для всех случаев. Например, если среднее время работы программы — 100мс, то большинство объектов можно вообще не закрывать и даже не собирать — ОС все сама почистит после завершения процесса.
Если ваша ОС — Windows, то она в любом случае почистит все после завершения процесса.
Вопрос не в том, чтобы «все почистить», а в том, чтобы писать корректный код, который «убирает за собой».
Финализатор просто помогает найти те объекты, которые не были обернуты в using, например
Зачем писать корректный код, который «убирает за собой», дублируя функциональность ОС?
Можно назвать несколько причин:
1. Это хорошая привычка. Не всегда вы пишите программы, которые потребляют мало ресурсов и поэтому можно мусорить где угодно
2. Компонентная архитектура. Сегодня ваш код используется в консольном приложении, а завтра — в высоконагруженном сервисе, где ресурсы можно и нужно освобождать побыстрее
3. Глубокое понимание системы. По моему опыту, программисты, которые не следят за ссылками/указателями/ресурсами поступают так не потому, что знают что-то о механизмах завершения процесса, а потому что просто не способны на это
4. Эстетическая составляющая. У каждой открывающей скобки должна быть закрывающая, а у каждого конструктора — деструктор
Эти все причины — лишь рекомендации. Я все же думаю, что не стоит запрещать программисту бросать объекты без вызова Dispose в короткоживущих процессах.

Кстати, вот придумал только что еще одну причину не бросать исключение в деструкторе, даже в DEBUG:
-2. Программист может пожелать сделать из intermediate window пару вызовов следующего плана: new SomeCriticalObject(1, 2, "foo.txt").Bar(); Но синтаксис intermediate window не позволяет сделать еще и Dispose без сохранения объекта в промежуточную переменную — а удобной переменной под рукой может и не оказаться.
     new SomeCriticalObject(1, 2, "foo.txt").Bar();

это не должно быть причиной. Такой интерфейс надо поменять и получить

     SomeCriticalObject.StaticBar(1,2,"foo.txt")


А внутри него все вполне можно за собой убрать.
Для добавления нового метода придется остановить отладку.

PS в прошлом сообщении была ошибка. immediate window. Но, судя, по отсутствию вопросов, все все поняли.
Это еще одна причина, по которой удаление временного файла следует делать по возможности системными средствами, а не в финализаторе.
Временный файл, созданный на диске, не будет «почищен».
Речь идет не о временных файлах, а о т.н. хендлах — идентификаторах объектов ядра, и о виртуальной памяти
А без P/Invoke это можно как то сделать?
А чем вас P/Invoke не устраивает?
Тем что он платформо зависимый.
К тому же процессы иногда живут долго: в моем случае месяцами. Так что приходится очищать все что можно и до завершения процесса.
Прочтите, но вы ветку читали? Здесь сейчас обсуждается вот этот случай:
Например, если среднее время работы программы — 100мс
К сожалению, AppDomainы работают слишком медленно.
в данном случае это не критично: таких тестов обычно не много.
Как сказать. Я вот однажды тоже в свою программу добавил несколько операций с доменами приложений. Предполагалось, что пользователь будет работать с программой долго, и пары секунд накладных расходов в фоновом потоке никто не заметит.

Но уже через 2 месяца эксплуатации выяснилось, что программа «слишком долго закрывается» — в 80% случаях ее запускали ради нажатия одной кнопки.
Спасибо. Буду иметь ввиду. Но в даннм случае, домен использовался только для теста.
Тестировать в вашем случае надо Dispose, а не финализатор (потому что в финализаторе у вас тривиальный код). Тогда и проблем будет намного меньше.
Именно так. Финализатор вообще не является контрактом класса, в отличии от Dispose.
и тем не менее я считаю нужным его тестировать
не совсем понимаю, почему я не должен тестировать финализатор: в нем есть какой то код (пусть и тривиальный). я хочу быть уверен, что используемый ресурс будет правильно «закрываться» в финализаторе, и что другой программист его не сломает.
Потому что количество усилий на это непропорционально результату.
в общем случае может быть, но в моем конкретном случае, несколько раз случались проблемы из за того, что ресурс не был освобожден, а тесты этого даже и не проверяли.
в любом случае идея этого поста: как тестировать финализатор, когда это надо.
А Dispose у вас при этом был покрыт тестами?
Да. Реализация Dispose покрыта тестом.
Значит нужно тестировать, что у вас явно вызывается Dispose (не из финализатора, а из кода-потребителя). Не диспозить disposable — зло.
Согласен на все 100%. Но никто не может мне гарантировать, что Dispose всегда будет вызываться. Следовательно, небольшое усилие (финализатор и его тест), по крайне мере, минимизуруют ущерб от не вызова Dispose.
Вопрос в том, что является небольшим усилием.

Загрузка и опускание аппдомена в каждом тесте — это не «небольшое усилие».
Согласен. Но в данном случае я имел ввиду усилия программиста приготовить такой тест.
В любом случае, таких тестов (для финализатора) не так много.
Ну да, а еще нарушение принципа единого места ошибки — поскольку вы не можете проверить, был ли вызван Dispose, вы проверяете, выполнил ли Dispose свою работу, что приводит к дублированию проверяющего кода.
Не понимаю: я проверяю, что Dispose, когда он вызывается «закрывает» ресурс. С другой стороны, я проверяю, что когда Dispose не вызывается, то финализатор «закрывает» ресурс. Где тут дублирование, с учетом, что это не совсем unit тест?
Дублирование в том, что вы дважды проверяете, что ресурс закрывается (и это может быть сложная проверка).

(кстати, ничто не мешает сделать это unit-тестом, в общем-то)
Интересное замечание.
Надо попробовать реализовать unit тест для данного примера.
Спасибо.
Может лучше было потратить усилия на то чтобы тестировать вызов Dispose во всех местах где он должен вызываться?
Хорошо бы. И я пытаюсь это делать. Но как я могу быть уверен на 100%, что Dispose всегда вызывается? Что он и завтра всегда будет вызываться?
Так вы и на 100% не можете быть увереным что финализатор вообще вызовется. Платформа вам этого не гарантирует.
По крайней мере в тех случаях, когда финализатор вызывается, я буду уверен, что он делает то, что я от него ожидаю.
И как я понимаю, в подавляющем числе случаев он все таки вызывается.
Так если вы протестируете логику Dispose, то автоматически протестируете и логику финализатора. А то, что вы делаете, так это проверяете, что финализатор срабатывает, т.е. пишете юнит-тест не на свою бизнес-логику, а на CLR. С таким подходом имеет смысл и такие тесты писать.
int i = 5
Assert.True(i == 5)
Не совсем, я проверяю, что финализатор вызывает нужный мне код.
Помоему, в старых версиях финализатор еще вызывался из своего высокоприоритетного потока, поэтому у ms еще был хитрый паттерн, если хочется использовать финализатор в связке с Dispose.
А в чем заключалась его хитрость-то?
Sign up to leave a comment.

Articles