Не так давно мы работали над диагностикой, связанной с проверкой финализатора, и у нас с коллегой возник спор по поводу деталей работы сборщика мусора и финализации объектов. И хотя я и он занимаемся разработкой на C# более 5 лет, к общему мнению мы не пришли, и я решил изучить этот вопрос подробнее.



Введение


Обычно первое знакомство с финализаторами у .NET разработчиков происходит, когда им нужно освободить неуправляемый ресурс. Возникает вопрос что же нужно использовать: реализовать в своём классе IDisposable или добавить финализатор? Тогда они идут, например, на StackOverflow и читают ответы на вопросы типа этого Finalize/Dispose pattern in C# где рассказывается про классический паттерн реализации IDisposable в сочетании с определением финализатора. Тот же самый паттерн можно найти и в MSDN в описании интерфейса IDisposable. Некоторые считают его довольно сложным для понимания и предлагают свои варианты вроде реализации очистки управляемых и неуправляемых ресурсов в отдельных методах или создания класса-обёртки специально для освобождения неуправляемого ресурса. Их можно найти на той же страничке на StackOverflow.

Большинство этих способов предполагают реализацию финализатора. Посмотрим какие плюсы и потенциальные проблемы это может принести.

Плюсы и минусы использования финализаторов


Плюсы.

  1. Финализатор позволяет произвести очистку объекта перед тем как он будет удалён сборщиком мусора. Если разработчик забыл вызвать у объекта метод Dispose(), то в финализаторе можно освободить неуправляемые ресурсы и таким образом избежать их утечки.

Пожалуй, всё. Это единственный плюс, да и то спорный, о чём ниже.

Минусы.
  1. Финализация недетерминированна. Вы не знаете, когда будет вызван финализатор. Прежде чем CLR начнёт финализировать объекты, сборщик мусора должен поместить их в очередь объектов, готовых к финализации, когда запустится очередная сборка мусора. А этот момент не определён.

  2. В связи с тем, что объект с финализатором не удаляется сборщиком мусора сразу, он и весь граф связанных с ним объектов переживают сборку мусора и попадают в следующее поколение. Удалены они будут теперь тогда, когда сборщик мусора решит собрать объекты этого поколения, что может произойти очень нескоро.

  3. Так как финализаторы выполняются в отдельном потоке параллельно работе других потоков приложения, то может возникнуть ситуация, когда новые объекты, требующие финализации, могут создаваться быстрее, чем будут отрабатывать финализаторы старых объектов. Это приведёт к увеличению потребляемой памяти, снижению производительности и, возможно, в итоге к падению приложения с OutOfMemoryException. Причём на машине разработчика вы можете никогда и не столкнуться с этой ситуацией, например, потому что у вас меньшее количество процессоров и объекты создаются медленнее или приложение работает не так долго, как в боевых условиях, и память не успевает закончиться. Можно потратить очень много времени на то чтобы понять, что причина была в финализаторах. Этот минус, пожалуй, перекрывает преимущества единственного плюса.

  4. Если при выполнении финализатора возникнет исключение, то выполнение приложения экстренно завершится. Поэтому при реализации финализатора нужно быть особенно аккуратным: не обращаться к методам других объектов, для которых уже мог быть вызван финализатор; учитывать, что финализатор вызывается в отдельном потоке; проверять на null все другие объекты, которые потенциально могли принимать значение null. Последнее правило связано с тем, что финализатор может быть вызван для объекта в любом его состоянии, даже не до конца проинициализированном. Например, если вы всегда присваиваете в конструкторе новый объект в поле класса и потом ожидаете, что в финализаторе он всегда должен быть не равен null и обращаетесь к нему, то можно получить NullReferenceException, если при создании объекта в конструкторе базового класса возникло исключение и до выполнения вашего конструктора дело не дошло.

  5. Финализатор может быть вообще не выполнен. При экстренном завершении приложения, например, при возникновении исключения в чужом финализаторе по причинам, описанным в предыдущем пункте, все остальные финализаторы не будут выполнены. Если вы в финализаторе освобождаете неуправляемые объекты операционной системы, то ничего плохого не произойдёт в том смысле что при завершении приложения система сама вернёт свои ресурсы. Но если вы сбрасываете недозаписанные байты в файл, то вы потеряете свои данные. Так что возможно лучше не реализовывать финализатор, а всегда допускать потерю данных в случае если забыли вызвать Dispose(), так как в этом случае проблему будет проще обнаружить.

  6. Нужно помнить о том, что финализатор вызывается только один раз и если вы воскрешаете объект в финализаторе путём присваивания ссылки на него в другой живой объект, то возможно вам следует зарегистрировать его для финализации заново с помощью метода GC.ReRegisterForFinalize().

  7. Вы можете нарваться на проблемы многопоточных приложений, например, состояние гонки, даже если ваше приложение однопоточное. Случай совсем уж экзотический, но теоретически возможный. Допустим в вашем объекте есть финализатор, и на него держит ссылку другой объект, у которого тоже есть финализатор. Если оба объекта становятся доступными для сборщика мусора, и их финализаторы начинают выполняться и другой объект воскрешается, то он и ваш объект снова становятся живыми. Теперь возможна ситуация, когда метод вашего объекта будет вызван из основного потока и одновременно из финализатора, так как он по-прежнему остался в очереди объектов, готовых к финализации. Код, который воспроизводит этот пример, приведён ниже. Можно увидеть как сначала выполняется финализатор объекта Root, потом финализатор объекта Nested, и после этого метод DoSomeWork() вызывается сразу из двух потоков.

Код примера
class Root
{
    public volatile static Root StaticRoot = null;
    public Nested Nested = null;

    ~Root()
    {
        Console.WriteLine("Finalization of Root");
        StaticRoot = this;
    }
}
class Nested
{
    public void DoSomeWork()
    {
        Console.WriteLine(String.Format(
            "Thread {0} enters DoSomeWork",
            Thread.CurrentThread.ManagedThreadId));
        Thread.Sleep(2000);
        Console.WriteLine(String.Format(
            "Thread {0} leaves DoSomeWork",
            Thread.CurrentThread.ManagedThreadId));
    }
    ~Nested()
    {
        Console.WriteLine("Finalization of Nested");
        DoSomeWork();
    }
}

class Program
{
    static void CreateObjects()
    {
        Nested nested = new Nested();
        Root root = new Root();
        root.Nested = nested;
    }
    static void Main(string[] args)
    {
        CreateObjects();
        GC.Collect();
        while (Root.StaticRoot == null) { }
        Root.StaticRoot.Nested.DoSomeWork();
        Console.ReadLine();
    }
}

Вот что будет выведено на экран на моей машине:

Finalization of Root
Finalization of Nested
Thread 10 enters DoSomeWork
Thread 2 enters DoSomeWork
Thread 10 leaves DoSomeWork
Thread 2 leaves DoSomeWork

Если у вас финализаторы вызываются в другом порядке, попробуйте поменять местами создание nested и root.

Выводы


Финализаторы в .NET — это то место, где проще всего выстрелить себе в ногу. Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны. Надо отметить, что и сами разработчики CLR предостерегают от их использования на странице Dispose Pattern: «Avoid making types finalizable. Carefully consider any case in which you think a finalizer is needed. There is a real cost associated with instances with finalizers, from both a performance and code complexity standpoint.»

Но если вы всё-таки решили использовать финализаторы, то PVS-Studio может помочь вам найти потенциальные ошибки. У нас есть диагностика V3100, которая покажет все места в финализаторе, где может возникнуть NullReferenceException.
Поделиться с друзьями
-->

Комментарии (56)


  1. poxu
    07.10.2016 11:34
    -4

    Интересная история. В Java тоже есть финализаторы и их категорически не рекомендуется использовать. И такого, чтобы разработчики с пятилетним опытом об этом не знали просто не может быть. Возраст платформы наверное сказывается.


    1. MonkAlex
      07.10.2016 11:40
      +4

      Ими просто никто особо не пользуется. Вот и знающих, как оно работает, не густо.

      Собственно, не вижу в этом проблемы.


  1. kekekeks
    07.10.2016 12:04
    +11

    Проблема с финализаторами в том, что одни их неправильно используют, а другие помимо того ещё и пишут статьи типа этой. А надо всего-то в классе с финализатором не держать ничего кроме ссылки на неуправляемый ресурс, а в идеале просто унаследовать SafeHandle.


    1. Bonart
      07.10.2016 13:03
      +4

      Эти же люди помимо статей пишут еще и статические анализаторы кода.


    1. poxu
      07.10.2016 13:25
      +1

      Я же правильно понимаю, что Finalize вызовется только когда сработает сборщик мусора, да и то с оговорками?


      1. Dywar
        07.10.2016 19:38

        Когда объект будет помечен на сборку, перейдет в следующее поколение, будет обработан специальным потоком финализации.
        В книгах расписано все подробно.

        Андерс Хейлсберг. Язык программирования C#, 4-е издание.

        Компилятор C# создает из деструктора финализатор. Внутрь финализатора добавляется блок try-finally, и финализатор базового класса вызывается в блоке finally.

        Если во время выполнения деструктора происходит исключение, и оно не перехватывается, выполнение деструктора завершается и вызывается деструктор базового класса (если он есть). Если базового класса нет (как в случае с типом object) или в нем отсутствует деструктор, исключение игнорируется.


        1. Dywar
          07.10.2016 20:11

          Исключение игнорируется не всегда, зависит от версии .NET и настройки политики обработки исключений.


          1. poxu
            08.10.2016 16:10
            -1

            Ну то есть пользоваться для освобождения ресурсов этим механизмом нельзя. А, если ресурс ещё занят на момент включения финализатора, то лучшее, что можно сделать это наверное показать пользователю большую красную табличку и ещё отослать разработчикам письмо, что всё сломалось. Но, как я понимаю, надеяться, что этот механизм нормально отработает тоже наивно.


  1. igorch96
    07.10.2016 12:19
    +1

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


  1. Bonart
    07.10.2016 12:58
    +2

    Статья представляет собой смесь дезинформации с рекламой.
    Финализаторы нужны только для аварийной очистки системных ресурсов, дабы утечки в вашем проекте не поставили в неудобную позу всю машину — об этом ни слова.
    Непосредственно их использовать скорее вредно, чем бесполезно, вот только причина совсем другая:
    у наследников CriticalFinalizerObject гарантии выполнения кода завершения лучше, чем у финализатора.
    Еще немного матчасти
    Особенно печально, что все это уже было на хабре
    Очень грустно, если уровень познаний разработчиков PVS Studio о .NET соответствует уровню данной статьи.


    1. DieselMachine
      07.10.2016 14:53
      -2

      Ну и где здесь дезинформация?
      Чтобы не было утечек неуправляемых ресурсов, нужно их самому освобождать через Dispose(), а не надеяться на финализаторы.


      1. Bonart
        07.10.2016 15:15
        +2

        Возникает вопрос что же нужно использовать: реализовать в своём классе IDisposable или добавить финализатор?

        Нет такого вопроса и быть не должно — финализатор и Dispose решают принципиально разные задачи.


        Большинство этих способов предполагают реализацию финализатора
        … Если разработчик забыл вызвать у объекта метод Dispose(), то в финализаторе можно освободить неуправляемые ресурсы и таким образом избежать их утечки.
        Пожалуй, всё.

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


        Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны.

        А вот здесь чистейшая деза. Финализаторы не нужны по совсем другой причине — для аварийной очистки есть более надежное место. И, разумеется, классам, не владеющим неуправляемыми ресурсами, финализаторы (и их аналоги) не нужны от слова совсем.


        1. DieselMachine
          07.10.2016 15:56

          Нет такого вопроса и быть не должно — финализатор и Dispose решают принципиально разные задачи.

          Понятно что не должно быть, но такие вопросы задают например на stackoverflow.

          Вы скрываете (или сами не знаете) единственную цель финализатора — спасти от последствий утечек не проект (для него в момент вызова уже поздно), а систему в целом.

          Спасаясь таким образом от утечек, вы скрываете проблему и её становится сложнее обнаружить

          А вот здесь чистейшая деза. Финализаторы не нужны по совсем другой причине — для аварийной очистки есть более надежное место. И, разумеется, классам, не владеющим неуправляемыми ресурсами, финализаторы (и их аналоги) не нужны от слова совсем.

          Где здесь деза я не понял. Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен, потому что его предназначение «Provides a mechanism for releasing unmanaged resources.»


          1. Johan
            07.10.2016 16:38
            +1

            Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен

            Это не совсем верно, IDisposable нужен, в том числе, и если вы владеете управляемыми ресурсами реализующими IDisposable. Например Stream.


            Финализатор, в свою очередь, нужен в случае, если вы работаете напрямую с IntPtr.


            1. DieselMachine
              07.10.2016 16:55
              -1

              Согласен. Но здесь у вас фактически тоже неуправляемый ресурс, только обёрнутый в IDisposable.


              1. PsyHaSTe
                07.10.2016 17:40
                +1

                У Stream есть свой финализатор, который о нем позаботится. А вот о неуправляемом ресурсе, от которого все что есть — IntPtr с каким-то адресом система не знает ничего.


                1. DieselMachine
                  07.10.2016 18:12

                  Если у вас голый IntPtr, то его нужно освободить в Dispose(), потому что это неуправляемый ресурс.
                  Если у вас объект типа Stream, то у него тоже нужно позвать Dispose() в своём Dispose(), потому что Stream — это фактически обёртка над неуправляемым ресурсом.
                  Есть там у Stream свой финализатор или нет, какая разница? В своём Dispose() освобождаем всё что нужно, вот и всё.


                  1. PsyHaSTe
                    07.10.2016 18:27

                    Финализатор пишется на случай, когда мы НЕ МОЖЕМ вызвать диспоз. Например когда у нас синглтон, который при закрытии приложения должен корректно завершить работу (записать об этом в базу, например), а пользователь жестко убивает процесс. Финализатор в этом случае как правило отрабатывает, диспоз — нет.

                    Выше уже писали, что у финализатора и диспоза разный смысл. Диспоз высвобождает ресурсы детерменировано, когда его попросили об этом. Финализатор нужен когда гроб гроб череп крест — приложение падает, и нужно хоть как-то сохранить данные.


                    1. Paull
                      07.10.2016 18:42

                      Например когда у нас синглтон, который при закрытии приложения должен корректно завершить работу (записать об этом в базу, например), а пользователь жестко убивает процесс. Финализатор в этом случае как правило отрабатывает, диспоз — нет.

                      Не думаю, что у финализатора больше шансов отработать, чем у dispose'а в случае действительно «жёсткого» завершения работы, например через TerminateProcess. Вот хороший комментарий на эту тему.


                      1. PsyHaSTe
                        07.10.2016 18:58

                        Ну, на практике финализатор реально пригождается.

                        Совершенно реальный пример: есть консольное приложение. Когда оно завершается по ctrl+c всё хорошо, корректно обрабатывается, диспозы, все дела, все отлично.

                        Как только пользовател закрывает через крестик (а большинство делает именно так), то все становится печально. Есть конечно событие OnConsoleShutdown, и в нем та же диспоз логика есть, но все события в нем не всегда успевают отработать, и начинают вызываться финализаторы. Они тоже ограничены по времени, но есть хотя бы шанс на то, что хоть что-то сохранят. Иногда они не могут (связные объекты были удалены раньше, чем родительские), но в большей части случаев все же отрабатывают.

                        Ну и да, я согласен, что такой механизм попахивает, но блин, дайте тогда альтернативу. Критика она хороша, но когда единственный воркэраунд это написание kernel-mode кода, в проекте на шарпе, то я лучше уж буду писать финализаторы.


                  1. Johan
                    07.10.2016 19:12

                    Если у вас голый IntPtr, то его нужно освободить в Dispose(),

                    Освободить в финализаторе, а в Dispose() освободить и вызвать GC.SuppressFinalize(this), поскольку ресурс уже освобождён и финализация не требуется. Таким нехитрым образом мы обеспечиваем быструю сборку объекта без очереди на финализацию.


                    При работе со Stream финализатор, разумеется, вообще не нужен.


                    1. DistortNeo
                      07.10.2016 19:39

                      Грамотным решением при работе с IntPtr было бы унаследоваться от SafeHandle, а не писать велосипед.


                      1. Johan
                        07.10.2016 20:28

                        Ну, он там примерно так и делает, внутри, вероятно. В общем, да, писать это не нужно, но финализатор нужен именно для этого.


                  1. PashaPash
                    10.10.2016 16:13
                    +2

                    Разница в том, что
                    — голый IntPtr нужно освобождать и в IDisposable.Dispose(), и в финализаторе.
                    — Dispose у объекта типа Stream нужно вызывать только в Dispose().

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

                    Проверка на null перед обращением спасет чуть более чем никак.


                    1. DieselMachine
                      11.10.2016 10:47

                      Так я и не предлагаю писать финализаторы, и тем более обращаться там к стриму.

                      MemoryStream реализует IDisposable, но не держит неуправляемый ресурс только потому что он является наследником Stream. А если бы он не был его наследником, то и IDisposable у него реализовывать смысла не было бы. Если у вас есть просто Stream и вы не знаете что там, ну или в общем случае объект, реализующий IDisposable, то следует предполагать что там внутри может оказаться неуправляемый ресурс, и поэтому нужно в конце явно позвать Dispose() у этого объекта.
                      Я не согласен с тем, что закрыть хэндл это проблемы стрима. Это проблемы того кто использует стрим — освободить его через Dispose().


                      1. Bonart
                        11.10.2016 14:16

                        следует предполагать что там внутри может оказаться неуправляемый ресурс, и поэтому нужно в конце явно позвать Dispose() у этого объекта

                        Вы еще скажите что Subscribe в реактивках возвращает внутри неуправляемый ресурс.
                        На самом деле не следует делать никаких предположений о том, что именно скрывается за IDisposable


                        Я не согласен с тем, что закрыть хэндл это проблемы стрима.

                        Закрыть стрим — проблемы того, кто им пользуется. Освободить неуправляемый ресурс, даже если у самой обертки Dispose не вызван — ее (обертки) прямая обязанность.


                        1. Johan
                          11.10.2016 20:25

                          Золотые слова.


                        1. DieselMachine
                          12.10.2016 00:05
                          -1

                          Сразу скажу, что Reactive Extensions я никогда не использовал, поэтому могу ошибаться. Я так понял что IDisposable, который возвращается из Subscribe, нужен для того чтобы потом отписаться от нотификаций через вызов Dispose(), а в RxJava аналогичный метод называется unsubscribe(). То есть если мне не нужно отписываться, то я могу и не звать Dispose(). Я не уверен что они правильно выбрали интерфейс IDisposable для этих целей, потому что IDisposable предполагает что ресурс в конце надо задиспозить, а здесь вроде как можно этого и не делать, что сбивает с толку и приводит вот к таким вопросам To Dispose or not to Dispose, that is the question?

                          Никто не обязан освобождать неуправляемые ресурсы в финализаторе. Если я считаю что финализатор принесёт больше проблем чем пользы, то я могу вообще его не добавлять, а возложить всё это на плечи того кто использует мой объект. В качестве примера вот вам цитата из вашей статьи: «The StreamWriter class owns a Stream object; StreamWriter.Close will flush its buffers and then call Stream.Close. However, if a StreamWriter was not closed, its finalizer cannot flush its buffers. Microsoft „solved“ this problem by not giving StreamWriter a finalizer, hoping that programmers will notice the missing data and deduce their error.»


                          1. Bonart
                            12.10.2016 11:31

                            Никто не обязан освобождать неуправляемые ресурсы в финализаторе.

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


                            В качестве примера вот вам цитата из вашей статьи

                            Это ничего, что речь идет не об освобождении ресурсов, а о сохранении изменений?
                            И кавычки вокруг "solved" тоже наверно просто так добавлены?


                            То есть если мне не нужно отписываться...

                            То это означает либо неявное предположение о совпадении времени жизни публикатора и подписчика, либо утечку. Чаще всего второе.


                            1. DieselMachine
                              12.10.2016 12:57

                              Это должно быть напечатано крупным шрифтом на главной странице вашего статического анализатора.

                              Причём тут вообще наш анализатор? Он не имеет отношения к этому обсуждению.

                              Это ничего, что речь идет не об освобождении ресурсов, а о сохранении изменений?

                              Этот пример про StreamWriter к тому что если финализатор создаст больше потенциальных проблем чем пользы, то от него стоит отказаться. Solved там в кавычках разумеется не просто так, а потому что проблема на самом деле ни разу не solved. Вот что им мешало сбросить недозаписанные байты в файл в финализаторе? Да ничего не мешало, технически это можно сделать, вот только выстрелить себе в ногу при этом очень просто, поэтому и не стали.

                              Никто не обязан освобождать неуправляемые ресурсы в финализаторе.

                              Вот вам ещё мнение Эрика Липперта по поводу финализаторов:
                              Sharp Regrets: Top 10 Worst C# Features
                              When everything you know is wrong, part one
                              When everything you know is wrong, part two
                              This feature is confusing, error-prone, and widely misunderstood.… And in most cases, use of the feature is dangerous, unnecessary, or symptomatic of a bug.
                              It is therefore very difficult indeed to write a correct finalizer, and the best advice I can give you is to not try.


                              1. Bonart
                                13.10.2016 09:18

                                Причём тут вообще наш анализатор? Он не имеет отношения к этому обсуждению.

                                Как не имеет? В нем не будет правил, касающихся финализаторов?


                                Этот пример про StreamWriter к тому что если финализатор создаст больше потенциальных проблем чем пользы, то от него стоит отказаться. Solved там в кавычках разумеется не просто так, а потому что проблема на самом деле ни разу не solved.

                                То есть отказ от использования финализаторов проблему не решил.


                                It is therefore very difficult indeed to write a correct finalizer, and the best advice I can give you is to not try.

                                Есть только один маленький нюанс — чтобы не пробовать писать финализатор на законных основаниях, достаточно использовать SafeHandle и его потомков.


                                PS: мне ни разу не пришлось писать финализатор. Но сложность его написания аргументом не была никогда.


                                1. Paull
                                  13.10.2016 10:26

                                  То есть отказ от использования финализаторов проблему не решил.

                                  А использование финализатора решило бы проблему? Нет, оно потенциально только замаскировало бы её. А вот новые ошибки при неправильно написанном финализаторе вполне можно создать.


                      1. PashaPash
                        11.10.2016 22:45

                        Закрыть хэнл, если вдруг он оказался открыт на этапе финализации — это проблемы стрима.

                        Дело не в том, что вы предлагаете писать или не писать финализаторы. Дело в том, через весь текст статьи проходит мысль «Dispose и Finalize непрерывно связаны, для IDisposable всегда нужно добавлять финализатор».

                        Т.е. вы вроде и не предлагаете писать финализаторы, но вывод статьи читается вот так:

                        Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны…

                        Но если вы всё-таки решили использовать финализаторы [для всех классов, реализующих IDisposable, о которых мы тут писали выше], то PVS-Studio… покажет все места в финализаторе, где может возникнуть NullReferenceException [при обращении из финализатора ко вложенным объектам ради вызова у них Dispose].


                        И вот эти намеки на «есть IDisposable — реализуй финализатор!» продолжаются в комментариях. Вам

                        Финализатор, в свою очередь, нужен в случае, если вы работаете напрямую с IntPtr.


                        А вы вроде как и соглашаетесь, но продолжаете намекать на то, что стрим — это неуправляемый ресурс (а неуправляемые ресурсы надо контролировать через финализатор, это все знают!):

                        Согласен. Но здесь у вас фактически тоже неуправляемый ресурс, только обёрнутый в IDisposable.


                        Т.е. я то понимаю, как работает Dispose / Finalize. И что есть разница между прямым (IntPtr) и косвенным (IDisposable) владением неуправляемым ресурсом. И вы скорее всего понимаете. И остальные в этом треде понимают. Но это не делает статью лучше, а ворнинг — адекватнее.


                        1. DieselMachine
                          12.10.2016 00:18

                          Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны…

                          Я так написал, потому что если вы зайдёте на страницу IDisposable в MSDN, например, то там сразу и пример, в котором используется Dispose() вместе с финализатором. И некоторые программисты просто берут и копируют этот пример к себе, а когда спрашиваешь у них зачем тут финализатор, отвечают — потому что паттерн такой.


                          1. Bonart
                            12.10.2016 11:33

                            И ваша статья, в отличие от старой тепляковской, ничем не может им помочь, так как не раскрывает суть проблемы.


                          1. Bonart
                            12.10.2016 11:38

                            Пример в MSDN относится к классу, владеющему управляемыми и неуправляемыми ресурсами одновременно.
                            Решается это просто: владение управляемым ресурсом — отдельная ответственность и этим занимаются отдельные классы. Все остальные классы неуправляемыми ресурсами не владеют, следовательно в финализаторах не нуждаются.


          1. Bonart
            07.10.2016 17:09

            Понятно что не должно быть, но такие вопросы задают например на stackoverflow.

            И вы даете на него ответ, основанный на заведомо неверной информации.


            Спасаясь таким образом от утечек, вы скрываете проблему и её становится сложнее обнаружить

            Получение управления финализатором — уже 100% наличие утечки. Сжигать весь дом необязательно — пожарная сигнализация в вашей комнате уже сработала.


            Где здесь деза я не понял.

            Это печальнее всего. Вы дали заведомо неверную причину для отказа от использования финализаторов. И не поняли этого, имея перед глазами комментарии со ссылками на подробные разъяснения.


            Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен,

            Видите ли, классы, реализующие IDisposable — управляемые ресурсы. Но их владельцы обязаны реализовать IDisposable сами. Такой вариант — 99% случаев. И это, в отличие от финализаторов, часть Junior-минимума
            Об управляемых и неуправляемых ресурсах


        1. Paull
          07.10.2016 16:05

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

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


          Не очень понятно, для чего по вашему мнению всё-таки подходит использование финализаторов.

          Непосредственно их использовать скорее вредно, чем бесполезно


          Как по вашему это противоречит содержанию статьи?


          1. Bonart
            07.10.2016 17:13

            Не очень понятно, для чего по вашему мнению всё-таки подходит использование финализаторов.

            Ни для чего, поскольку для их целевого назначения есть более подходящее средство.


            Как по вашему это противоречит содержанию статьи?

            Статья дает рекомендации на базе заведомо неверной информации.


            1. Paull
              07.10.2016 17:31
              +1

              Статья дает рекомендации на базе заведомо неверной информации.

              Как я понимаю, вы про это:
              Непосредственно их использовать скорее вредно, чем бесполезно, вот только причина совсем другая:
              у наследников CriticalFinalizerObject гарантии выполнения кода завершения лучше, чем у финализатора.

              Статья должна была сказать, что использовать финализаторы не нужно, потому что есть CriticalFinalizerObject, и нужно всегда использовать его? А разве CriticalFinalizerObject лишён всех минусов, которые перечислены в статье? Может быть статью стоило назвать тогда «Почему не стоит использовать CriticalFinalizerObject»?


              1. Bonart
                07.10.2016 17:53

                Статья должна была сказать...?

                В статье не должно быть дезинформации.


                Может быть статью стоило назвать тогда «Почему не стоит использовать CriticalFinalizerObject»?

                Тогда дезинформация будет прямо в заголовке.
                Использовать CriticalFinalizerObject (обычно в виде наследования от одного из его потомков) в классах-обертках для неуправляемых ресурсов не то что "не стоит", а необходимо.


                1. Paull
                  07.10.2016 18:01
                  -1

                  Т.е. если я реализую свой класс, наследуя его от CriticalFinalizerObject и в нём реализую собственный финализатор, то он будет застрахован от всех проблем, описанных здесь? При этом я не говорю сейчас про использование стандартных классов-наследников CriticalFinalizerObject наподобие того-же SafeHandle, тема статьи именно реализация собственных финализаторов.


                  1. Bonart
                    07.10.2016 22:42

                    Т.е. если я реализую свой класс, наследуя его от CriticalFinalizerObject и в нём реализую собственный финализатор, то он будет застрахован от всех проблем, описанных здесь?

                    А это от вас зависит.
                    Надо заметить, что прямой наследник от CriticalFinalizerObject находится весьма далеко в рекомендуемом дереве решений:


                    1. Не владеем ничем что умеет IDisposable — IDisposable нам и самим не нужен
                    2. Владеем только управляемым — нужен IDisposable и ничего более
                    3. Хотим владеть неуправляемым — если есть готовая обертка с IDisposable, то смотрим предыдущий пункт, если нет, то следующий.
                    4. Делаем управляемую обертку для неуправляемого ресурса — используем максимально специализированный потомок CriticalFinalizerObject.
                    5. Нет подходящего потомка — делать нечего, придется наследоваться от CriticalFinalizerObject напрямую и переопределять метод Finalyze

                    Жесткие требования к завершающему коду не являются основанием для отказа его писать.
                    Правда у меня за все время работы такой необходимости не возникло ни разу.


                    1. Paull
                      08.10.2016 10:49
                      +1

                      Согласен с приведённым вами списком. Вот и автор, на мой взгляд, говорит о том, что для большинства ситуаций подходят пункты 3-4, и не стоит сразу лезть в пункт 5 (а вовсе не противопоставляет IDisposable и финализаторы), как и вы сами:

                      Правда у меня за все время работы такой необходимости не возникло ни разу.


                      Если же вы взялись делать пункт 5, то
                      А это от вас зависит.

                      перечислены разные способы выстрелить себе в ногу.


                      1. Bonart
                        08.10.2016 18:44

                        Вот и автор, на мой взгляд, говорит о том, что для большинства ситуаций подходят пункты 3-4,

                        У автора вообще нет ни одного из этих пяти пунктов, я не знаю каким образом вы их вывели из текста статьи.
                        Зато есть дезинформация про якобы "единственный спорный плюс", а жесткие требования к реализации представлены как "минусы", из-за которых финализаторы писать вроде как не нужно.
                        Рекомендация в заключении...


                        Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны.

                        … просто вредная. Понять, нужен ли финализатор, не просто, а очень просто. Думать же надо о соблюдении требований, когда явный код завершения все-таки нужен.


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


                        А больше всего огорчает то, что ресурсы компании прямо сейчас тратятся на защиту чести мундира вместо повышения квалификации собственных специалистов и улучшения качества продукции, включая рекламные материалы вроде данной статьи.


                        1. Paull
                          08.10.2016 23:57
                          +1

                          У автора вообще нет ни одного из этих пяти пунктов

                          Так статья и не ставит своей целью осветить все механизмы освобождения ресурсов
                          жесткие требования к реализации представлены как «минусы», из-за которых финализаторы писать вроде как не нужно.

                          А разве жёсткие требования не являются «минусами», раз могут приводить к реальным ошибкам? Статья не призывает вообще их не использовать, но что «нужно десять раз подумать» прежде чем это делать.
                          Понять, нужен ли финализатор, не просто, а очень просто.

                          Может быть для вас это и так, а на мой взгляд принять решение использовать что-то или нет — это ключевой момент, и перечисленные минусы ещё раз должны напомнить важность этого.
                          Ну и рекламируемый инструмент статического анализа вместо предупреждения о неверном способе реализации завершающего кода дает подсказки про NRE в финализаторах, которые вообще нет смысла писать.

                          По вашему потенциально ошибка, приводящая к неожиданному аварийному завершению приложения не является «неверным способом реализации»?
                          А больше всего огорчает то, что ресурсы компании прямо сейчас тратятся на защиту чести мундира

                          Если у вас есть конструктивные предложения, как можно улучшить наш продукт и какие ещё паттерны в реализации деструкторов можно детектировать, мы всегда готовы их выслушать.


                          1. Bonart
                            09.10.2016 03:15

                            У автора вообще нет ни одного из этих пяти пунктов

                            Так статья и не ставит своей целью осветить все механизмы освобождения ресурсов

                            Так все пять пунктов относятся строго к заявленной теме статьи: "Почему вам не следует использовать финализаторы". Каждый из них дает простой и конкретный ответ, почему в данном варианте использования финализатор не нужен. К "минусам" из статьи ни один из них отношения не имеет.


                            А разве жёсткие требования не являются «минусами», раз могут приводить к реальным ошибкам?

                            Нет, не являются.


                            Статья не призывает вообще их не использовать, но что «нужно десять раз подумать» прежде чем это делать.

                            Про 10 раз подумать — чистейшая деза. Ответ на сам вопрос "надо ли писать код аварийной очистки?" — простой, на уровне рефлексов. Реализация в пункте 5 сложная, но эта сложность не может служит оправданием отказа от аварийной очистки в обертках для неуправляемых ресурсов.
                            В результате статья:


                            1. Призывает к бесполезным размышлениям над тем, что имеет готовое решение
                            2. Поощряет плохой код там, где надо соблюдать требования, а не уклоняться от них.


    1. alexeiz
      07.10.2016 17:31
      +2

      все это уже было на хабре

      А рекламы PVS Studio ещё не было. То есть было, но мало. То есть совсем не мало, а прилично так рекламы этой PVS Studio. Ну, то есть одна сплошная реклама достала уже!


  1. DistortNeo
    07.10.2016 15:19
    +1

    > Если вы в финализаторе освобождаете неуправляемые объекты операционной системы, то ничего плохого не произойдёт в том смысле что при завершении приложения система сама вернёт свои ресурсы.

    Система-то ресурсы вернёт, а вот то, что упадёт приложение из-за утечки ресурсов — это и есть плохо.

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


  1. gurux13
    07.10.2016 15:32
    +2

        ~Root()
        {
            StaticRoot = this;
        }
    
    Какой же это страшный кошмар. Это, кажется, не выстрел себе в ногу, а добровольный прыжок в сверхновую.
    Расскажите пожалуйста, есть ли хотя бы один пример in the wild самоспасения в финализаторе? Откуда может возникнуть необходимость не дать себя удалить только когда тебя уже собрались удалять?


    1. DieselMachine
      07.10.2016 16:05

      Вот здесь например пишут о повторном использовании объекта таким способом. Сам я ни разу не встречал на практике


      1. DistortNeo
        07.10.2016 17:26
        +3

        Я правильно понимаю, что описанный паттерн — это костыль, предназначенный для исправления ошибок в коде, а именно отсутствия явного вызова Dispose?

        Лично я считаю допустимым только вариант, когда в финализаторе вызывается Dispose и ничто больше. При этом явный вызов Dispose должен приводить к подавлению вызова финализатора, чтобы не грузить лишний раз GC:

        public void Dispose()
        {
            …
            GC.SuppressFinalize(this);
        }

        ~Holder()
        {
            Dispose();
        }

        Воскрешать объект в финализаторе не нужно. Вместо этого стоит выделить логику тяжёлого ресурса и логику управления этим ресурсов в разные классы: один класс — ресурс, второй класс — холдер, который держит ссылку на ресурс и возвращает ресурс в пул при необходимости.

        Возможна ещё алтернативная реализация с использованием WeakReference — тогда вообще можно без финализаторов обойтись.


        1. DistortNeo
          07.10.2016 18:46

          Я вот грубую ошибку допустил, а вы и не заметили.

          Если ссылка на ресурс присутствует только у холдера, то оба эти объекта: и холдер, и ресурс, будут поставлены в очередь на вызов финализатора, что приведёт к негативным последствиям: объект будет переиспользован вызовом Dispose у холдера, но его ресурсы все равно освобождены, т.к. он из очереди на финализацию он не выйдет.

          Для того, чтобы этого избежать, фабрика, выделяющая ресурсы, должна хранить жёсткие ссылки на все выделенные объекты. Альтерантивный вариант в виде вызовов SuppressFinalize и ReRegisterForFinalize крайне нежелателен тем, что может легко привести к ошибкам: можно случайно вызвать финализатор 2 раза или не вызвать вообще. Нельзя так обращаться с системыми ресурсами.


  1. PashaPash
    09.10.2016 23:59
    +1

    Финализатор предназначен для освобождения неуправляемых ресурсов принадлежащих непосредственно текущему объекту. И больше ни для чего другого. Это, кстати, прямо сказано даже в MSDN:

    The Finalize method is used to perform cleanup operations on unmanaged resources held by the current object before the object is destroyed

    То, что в финализатор полез по ссылке на другой управляемый объект, скорее всего означает что разработчик не понимает разницы межу Dispose и Finalize, и пытается «освобожать» управляемые ресурсы. Или вообще пытается «помочь» сборщику мусора. Вероятность того, что он на самом деле гуру, и использует финализатор не по назначению сознательно, знает обо всех последствиях своего решения… и при этом не проверил на null — ничтожно мала.

    Адекватной реакцией коданализа на обращение к reference-свойству было бы «Ты точно знаешь что делаешь? Остановись, почитай про финализаторы! Если твой класс не контролирует неуправляемые ресурсы напрямую — удали финализатор и живи дальше! Если контролирует — используй SafeHandle!»

    Еще более адекватным было бы предупреждение на сам факт наличия финализатора. Со страшными минусами из статьи в описании. С отсылкой к тому же SafeHandle. Это действительно предотвратило бы основную ошибку разработчика — реализацию финализатора в случае, когда он совсем не нужен.

    А PVS вместо этого говорит разработчику «Все ок, делай финализатор, лазь в нем по другим объектам — это нормально. На null только проверь, и все будет хорошо!»


  1. user004
    10.10.2016 09:51
    +1

    Возникает вопрос что же нужно использовать: реализовать в своём классе IDisposable или добавить финализатор


    Это в каких задачах возникают такой вопрос?
    Видимо у меня слишком узкий круг задач. Давно не использовал финализатор.


    1. PsyHaSTe
      10.10.2016 19:46

      Во всех задачах, когда нужно освобождать ресурсов, а обертки в которой уже есть финализатор нет.