Pull to refresh

Comments 25

Не очень понятна проблематика.


Никогда не создавай язык с исключениями, в котором нет детерминированного освобождения ресурсов.

Громкое заявление. Читаем обоснование:


Причина по которой существует данное правило — детерминированное освобождение ресурсов необходимо для создания поддерживаемых программ.

Отсюда сразу следует, что недетерминированное освобождение не дает возможности создавать поддерживаемые программы. Т.е. если мы недетерминированно освобождаем память — то такие программы неподдерживаемые. А, значит, любые программы на языке с GC — неподдерживаемые.


Выглядит очень странно. Или я неправильно понял посыл?


И далее:


Детерминированное освобождение ресурсов обеспечивает определенную точку, в которой программист уверен, что ресурс освобожден.

Определенную точку это, может быть, и обеспечивает, только не определенное время, т.к. обычно используются не real-time OS. А раз значит разницы между так называемым "традиционным" и "современным" подходом в такой постановке сильно размывается.


Далее тоже немало перлов в таком же духе.

Автор статьи немного укушен C++ и льет много желчи. Я даже часть вырезал из перевода, чтобы совсем агрессивно не смотрелось. Всетаки в МС не совсем дураки и понимали последствия решений.

Вообще говоря поток финализатора один на приложение. О чём говорит автор, рассказывая о многопоточной финализации, не особо ясно.

Разве в спецификации точное количество потоков указано?

В gc.cpp указано. Что в дотнетном, что в моновском. Насчёт спецификации не упомню.

Давно уже придуман куда более простой способ реализации IDisposable. И только авторы документации для дезинформации никак не трогают устаревшую статью про полный паттерн IDisposable...


Все просто:


  1. любому неуправляемому ресурсу нужна обертка, наследующая от SafeHandle;
  2. всем остальным классам финализаторы не нужны.

Более простой способ в этой статье и описан. Это первоисточник того, что сейчас в MSDN и некоторых книгах.

«В этой» — это в какой? Почему-то не вижу его.

Потому что это первая часть. Об этом написано в начале и в конце.

В статье есть серьезная идеологическая ошибка про вызов финализатора. Он всегда происходит строго после того, как GC решил, что на объект никто не ссылается.


Отсюда получаем несколько ошибок:


Финализаторы вызываются в отдельном потоке и могут быть вызваны до того, как IDisposable.Dispose завершит работу. Нобходимо использование GC.SuppressFinalize чтобы избежать таких "гонок".

Нет. Если IDisposable.Dispose работает, то объект виден со стека, а значит GC его не прибьет, а значит финализатор не запустится.


Объект удаляется из очереди, и если на него больше никто не ссылается, то будет очищен при следующей сборке мусора.

Нет, объект будет очищен раньше. Еще раз: сначала сборка мусора, потом вызов финализаторов.


Далее — мне кажется, что Вы не до конца поняли идею IDisposable (или аналога AutoCloseable из Java). Суть такова, что вы можете освободить объекты до того, как их скушает сборщик мусора, если это даст плюсы программе. Например, Вы можете закрыть файл сразу после работы с ним, что приведет к тому, что другие программы смогут с ним работать.


Другой пример — вызов Dispose у Memory Stream из этой библиотеки. Он приводит к тому, что буферы из потоков можно начать использовать для других вещей (т.е. не требуется дополнительное перевыделение памяти)


Нормальное завершение в случае вызова IDisposable.Dispose и экстренное в противном случае.

См. выше. В этом и суть освобождения ресурсов. Мы можете "забыть" вызвать Dispose, однако ресурсы таки освободятся, в отличии от того же C. Считайте подобное необходимостью для более дешевой разработки больших приложений.


И еще недоработки статьи:


Класс StreamWriter владеет объектом Stream… Если StreamWriter не закрыт, буфер не сбрасывается и чатсь данных теряется. Microsoft просто не переопределил финализатор, таким образом "решив" проблему завершения.

Нет. В Finalize необходимо только убивать unmanaged ресурсы. StreamWriter не владеет ими. Так что GC сам найдет Stream и вызовет финализатор. Проблемы тут нет.


Еще раз: Disposable должен очищать всё каскадно. Финализатор объекта отвечает только за сам объект.


Программист должен не забывать вызывать IDisposable.Dispose для объектов в коллекции или создавать своих наследников классов коллекций, которые реализуют IDisposable чтобы обозначить "владение"

Вы про CompositeDisposable ?

Нет, объект будет очищен раньше. Еще раз: сначала сборка мусора, потом вызов финализаторов.

Не совсем так. Сначала сборка мусора, потом вызов финализатора, потом нужна еще одна сборка мусора. Если финализатор сохранит куда-нибудь this — то объект так и останется в памяти (это называется "возрождение", resurrection).

Нет. Если IDisposable.Dispose работает, то объект виден со стека, а значит GC его не прибьет, а значит финализатор не запустится.

Не обязательно. Сборщик мусора знает какие переменные на стеке используются, а какие нет.


Не знаю как сейчас, но раньше код:


class A 
{
   void B() 
   {
       GC.Collect();
       Console.WriteLine("A.B");
   }

   ~A() 
   {
       Console.WriteLine("A.Finalizer");      
   }
}

void Main()
{
    var a = new A();
    a.B();
}

Выдавал:


A.Finalizer
A.B
Нет. В Finalize необходимо только убивать unmanaged ресурсы. StreamWriter не владеет ими. Так что GC сам найдет Stream и вызовет финализатор. Проблемы тут нет.
Почему тогда StreamWriter.Close закрывает Stream по-умолчанию?
Еще раз: Disposable должен очищать всё каскадно. Финализатор объекта отвечает только за сам объект.

Статья как раз об этом. Только это не все понимают даже после того, как документацию поправили. Даже Рихтер в зеленой книге поправил после того, как она перестала быть зеленой.


Но в этом также проблема. С таким "контрактом" IDisposable не сделаешь нормальное завершение, что показывает нам класс StreamWriter. Каждый кто использовал StreamWriter сталкивался с проблемой потери данных.

Каким образом проблема метода Finalize мешает сделать нормальное завершение в методе Dispose?
Такое чувство, что у автора просто каша в голове. Говорит про детерминированное освобождение ресурсов, но при этом приводит в пример shared_ptr. Что же детерминированного в shared_ptr? Наоборот, мы не можем предсказать, когда именно объект удалится. Как раз Dispose — куда более детерминированный подход. Странно слышать критику подхода C# в таком ключе.
Не говоря уж о том, что странно приводить в пример C++, т.к. там вообще нет finally, и ничто не может гарантировать выполнение какого-то участка кода, в случае UB. Это является большой проблемой, т.к. нет вообще никакой возможности действительно гарантированно освободить ресурс. Концепция UB ломает подобные попытки на корню. Как ты не обвешивайся своими исключениями, shared_ptr'ами и прочим, одно единственное UB где-то в недрах используемых тобой библиотек, и всё пропало.
И при этом, автор делает гениальное предложение:
Вместо проверок на каждый чих, лучше считать обращение к объекту в «освобожденном» состоянии «неопределенным поведением», как обращение к освобожденной памяти.

Спасибо, но нет. Это вообще худшее что можно придумать. Такой подход может быть оправдан для низкоуровневого C, где производительность поставлена во главу угла. Но уже даже в C++ от этого наследства больше вреда, чем пользы. А уж неопределённое поведение в управляемом языке вроде C# — это вообще нонсенс. Как такое можно всерьёз предлагать? Да ещё в контексте детерминированного освобождения ресурсов.

По моему мнению, статья — перевод статьи десятилетней давности, это просто дурной тон, или вкус, или и то и другое. Тем более, когда есть более свежие статьи, на самом же хабре, в которых раскрыто больше информации чем в исходной. К примеру эта.

Неплохо написано в этой статье, но не описаны всякие Safe Handle и прочее. Еще вот тут.


В общих чертах


  • Класс может ничем не владеть (Dispose не нужен), владеть несколькими управляемыми ресурсами или владеть только одним неуправляемым ресурсом
  • Если класс владеет управляемыми ресурсами, то реализуйте IDisposable. Проверка на многократный вызов не нужна, потому что требуется, что многократный вызов Dispose допустим. И это транзитивно распространяется.
  • Для неуправляемого ресурса нужен IDisposable и финализатор.

А заявление, что ужасный GC вообще и IDisposable в частности как-то усложняют жизнь программистам кажется необоснованным. Если бы проблем от недетерменированного освобождения ресурсов было бы больше, чем от ручного управления памятью/RAII/подсчета ссылок, то никто бы и не использовал сборщики мусора.


Вопрос. Насколько я понимаю, подсчет ссылок может работать некорректно на циклических зависимостях, и тогда он не сможет их освободить вовсе, в отличие от GC?

Вопрос. Насколько я понимаю, подсчет ссылок может работать некорректно на циклических зависимостях, и тогда он не сможет их освободить вовсе, в отличие от GC?

Да, так и есть. Именно по этой причине в том же C++ пришлось придумывать невладеющий std::weak_ptr в дополнение к совместно владеющему std::shared_ptr.

Сборщики мусора и автоматическое управление памятью действительно таки усложняют жизнь в ряде случаев. Просто в области применения C# таких случаев действительно очень мало

Я не спорю. Просто разные языки для лучше подходят для разных задач, а единой панацеи от всего нет. Некоторые, кажется, не понимают.

В дополнение к статье. В отличии от конструкции using тип shared_ptr меньше подвержен проблемам протечки абстракции.


Если у вас есть достаточно сложный код, то весьма вероятно, что Dispose вызовется позже, чем мог бы быть вызван при оптимальном использовании.


А shared_ptr убивает объект моментально при отсутствии ссылок.

… но только если не было цикла. При наличии циклических ссылок shared_ptr не убьет объект никогда.
Only those users with full accounts are able to leave comments. Log in, please.