На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> Стандартная модель освобождения ресурсов .NET , статья написанная от скуки
    Представляется статья, в которой я попытаюсь осветить один из аспектов управления ресурсами на платформе .NET, а именно стандартной модели освобождения ресурсов посредством реализации интерфейса IDisposable в связке с завершителем (finalizer).

    Предлагаю, Вам маленький тест для самопроверки знаний:
    • Знаю ли я, что такое стандартная модель освобождения ресурсов?
    • Уверен ли я, что корректно реализую стандартную модель освобождения ресурсов в собственных типах?
    • Могу ли я объяснить, что такое завершитель объекта?
    • Знаю ли я, что такое неуправляемые ресурсы, а также какие требования к программному коду порождает их использование в наших программах?
    • Знаю ли я, где предпочтительней использовать завершитель, а где стоит реализовать интерфейс IDisposable?

    Последний вопрос был задан шутки ради. Как дальше мы с Вами увидим, завершитель всегда стоит использовать в связке с реализацией интерфейса IDisposable. Для тех, кто смог на первые четыре вопроса ответить положительно, а пятый вызвал у него бурю негодования и возмущения, предлагаю заняться более полезным занятием, чем чтение данного руководства. Остальным же, кто все-таки решился продолжить чтение или не смог утвердительно ответить на предлагаемые вопросы, предлагаю вместе детально рассмотреть те проблемы, которые возникают у программиста при работе с ресурсами в наших программах, выяснить причины возникновения этих проблем, а самое главное найти рецепт их решения.
    С самого начала моего знакомства с платформой .NET меня со страниц всех без исключения учебников безостановочно преследует тезис о том, что автоматический сбор мусора есть панацея от всех бед, связанных с освобождением используемых программой ресурсов. Но так ли это? Не дает ли это нам ложного ощущения, что достаточно создать объект, использовать его, а сборщик мусора, как всегда отлично, выполнит всю черновую работу по освобождению ресурсов? В подавляющем большинстве случаев так и будет. Сборщик мусора прекрасно удалит все ненужные объекты, которые размещались в памяти нашей программы. А как же неуправляемые ресурсы? Как же потоки операционной системы, объекты GDI, файлы и порты, соединения с базой данных? Ответ на этот вопрос, вероятно, знает каждый из Вас, их освобождение ложится на плечи самих программистов, то есть это и есть наша с Вами непосредственная обязанность. Следовательно и для нас с Вами найдется работа :) Каждый тип, оперирующий ресурсами, которые не могут быть освобождены сборщиком мусора, обязан предоставить нам с Вами механизм для их освобождения. В рамках платформы .NET этим механизмом обычно является реализация интерфейса IDisposable, который объявляет единственный метод Dispose, посредством вызова которого мы с Вами явно указываем объекту на то, что ему следует освободить неуправляемые ресурсы, которые он задействовал. Повторюсь, все объекты, которые используют неуправляемые ресурсы, обязаны предоставить механизм по их освобождению, обычно посредством реализации интерфейса IDisposable и предоставлению нам с Вами общедоступного метода Dispose. Из этого следует более чем очевидный вывод: если вы сами пишете тип, который явно или неявно оперирует неуправляемыми ресурсами, он также обязан предоставить клиенту возможность освободить занимаемые им ресурсы. Но можем ли мы с Вами гарантировать, что все клиенты нашего с Вами типа воспользуются методом Dispose? Мне бы хотелось верить, что это так, но к сожалению... Но не все так безнадежно :). Мы можем застраховаться на этот случай путем реализации завершителя в нашем типе. Что такое завершитель? Это специальный метод, который реализует наш тип, посредством которого сборщик мусора сможет выполнить освобождение неуправляемых ресурсов.
    ExpandedWrap disabled
      public class MyClass
          {
              ...
              // завершитель
              ~MyClass()
              {
                  // освобождаем неуправляемые ресурсы
                  
              }
              ...
       
          }


    Как видите объявление завершителя подобно объявлению конструктора типа, но с добавлением перед именем значка тильда(~). Думаю разработчики со стажем программирования на С++ могут сказать, что объявление завершителя аналогично объявлению деструктора, и это так. Тем не менее есть одно существенное отличие от языка С++, время вызова завершителя не определено (недетерминировано). Следовательно, мы не можем рассчитывать на то, что код по освобождению ресурсов выполнится непосредственно по выходу объекта из области видимости или вызова завершителя. Так когда же он отработает? В момент, когда подсистема сбора мусора инициирует уборку, сборщик мусора поступает следующим образом: он удаляет из памяти все объекты, у которых отсутствует завершитель. Далее для объектов с завершителем он создает отдельный поток, в котором создает очередь, куда помещает объекты, которые реализовали завершитель, и требуют очистки. Далее в порядке очередности сборщик мусора последовательно вызывает завершители для всех объектов, которые в ней находятся. Следует подчеркнуть, что их освобождение происходит в отдельном потоке, и окончание освобождения неуправляемых ресурсов не обязательно совпадает со временем окончания сбора для обычных объектов. Более того, только после отработки завершителя объект превращается в мусор, который будет убран при следующем проходе сборщиком. Исходя из этого, мы должны теперь для себя провести четкую грань между реализацией IDisposable и реализацией завершителя. Первый предоставляет клиенту класса механизм по явному освобождению ресурсов посредством общедоступного метода Dispose, второй страхует нас с Вами от нерадивых программистов, которые по тем или иным причинам забывают вызвать Dispose для нашего типа явно. А значит более чем очевидно, что правильный путь - это реализация интерфейса IDisposable и завершителя в связке.
    И так правильный путь найден, но какие проблемы порождает написание такого кода? Попробуем ответить на несколько вопросов.
    1. Что произойдет, если классы наследники захотят переопределить работу завершителя или реализовать IDisposable, согласно собственным потребностям?
    2. Как производный класс решит, освобождать ли ресурсы базового типа и то, как это следует реализовать?
    Очевидно, следует уведомить об этом базовый класс, иначе последствия такого неуведомления предсказуемы - ресурсы, используемые базовым типом, останутся неочищенными. Существует еще одна дилемма: код, реализуемый методом Dispose, обычно полностью или частично дублируется завершителем. Вот здесь нам на помощь и приходит стандартная модель очистки ресурсов. С ее помощью мы избавляемся и от дублирования кода, и предоставляем классам-наследникам возможность определять собственный механизм освобождения ресурсов, в совокупности с возможностью освобождать ресурсы базового класса. Эта модель реализуется посредством перегрузки метода Dispose следующим образом:
    protected virtual void Dispose(bool isDisposing)
    Как видно из прототипа, метод объявляется защищенным, а следовательно, он не предназначен для клиента нашего класса, он необходим нашему типу и типам наследникам. Наш тип будет использовать его в ответ на непосредственный вызов клиентом метода Dispose() или вызов завершителя инфраструктурой сбора мусора. Обратите внимание на то, что метод виртуальный, это позволит классам-потомкам переопределить поведение метода с учетом собственных потребностей, параллельно вызывая базовую реализацию метода для освобождения ресурсов класса-предка. Думаю, самые любопытные уже горят желанием узнать: зачем потребовался параметр типа bool? Все очень просто, если завершитель предназначен для освобождения неуправляемых ресурсов, то в методе Dispose зачастую реализуют механизм по освобождению управляемых ресурсов. Вы удивлены? Когда-то я тоже был удивлен. Чтобы понять суть происходящего, нужно хорошо разбираться в тонкостях некоторых процессов, которые происходят при сборке мусора на платформе .NET. Предположим, что наш тип является подписчиком на некоторое событие, в котором он заинтересован. События на платформе .NET являются сильными двунаправленными ссылками. То есть класс, подписавшийся на событие, не может быть освобожден раньше, чем будет освобожден класс, который генерирует события. Верно так же и обратное: класс, который генерирует событие, не может быть утилизирован подсистемой сборки мусора, пока он имеет хоть одного «живого» подписчика. Возникает дилемма, которая может быть разрешена исключительно с помощью механизма отписки от событий получателем. Именно поэтому код, отписывающийся от событий, следует также помещать в реализацию метода Dispose. Итак, в общем виде метод Dispose должен вызываться с параметром true для очистки по требованию клиентом и с параметром false при вызове завершителя. Основываясь на этом параметре, перегруженный метод Dispose принимает решение: стоит ли освобождать управляемые ресурсы (не управляемые ресурсы освобождаются в не зависимости от переданного параметра).
    Давайте посмотрим на реализацию IDisposable в нашем типе:
    ExpandedWrap disabled
      public class MyClass : IDisposable
          {
              ~MyClass()
              {
                  Dispose(false);
              }
       
              public void Dispose()
              {
                  Dispose(true);
                  GC.SuppressFinalize(this);
              }
          }

    После вызова перегруженной версии Dispose с параметром true, вызывается метод SuppressFinalize, который говорит подсистеме сборки мусора буквально следующие: «Объект был осовобожден явно, вызов завершителя для него более не требуется». По сути мы подавляем вызов завершителя для нашего типа.
    Остался последний нерешенный нами вопрос, что будет, если пользователь обратиться к методу Dispose несколько раз? Возможно ничего, а возможно это приведет к ошибке времени выполнения, поэтому мы перестраховываемся и добавляем в наш тип булеву переменную, которая сигнализирует, что наш тип уже был очищен. Ниже приводится шаблон реализации IDisposable в нашем типе:
    ExpandedWrap disabled
      public class MyClass : IDisposable
          {
              private bool _disposed = false;
       
              // завершитель
              ~MyClass()
              {
                  Dispose(false);
              }
       
              // очищаем ресурсы, после чего подавляем
              // вызов завершителя
              public void Dispose()
              {
                  Dispose(true);
                  GC.SuppressFinalize(this);
              }
       
              protected virtual void Dispose(bool isDisposing)
              {
                  // если метод уже вызывался,
                  // то не стоит выполнять очистку дважды
                  if (_desposed)
                      return;
       
                  if (isDisposing)
                  {
                      //освобождаем управляемые ресурсы
                  }
       
                  // особождение неуправляемых ресурсов
       
                  //установим флажок, что метод уже выполнялся
                  _disposed = true;
              }
          }


    Шаблон выше является примером реализации стандартной модели по освобождению ресурсов и должен применяться Вами при конструировании собственных типов. В заключение хотелось бы дать несколько советов. Все общедоступные методы вашего типа не должны выполняться после вызова метода клиентом метода Dispose(), это легко реализовать путем проверки переменной _desposed в теле метода. В случае если объект уже был освобожден, необходимо сгенерировать исключение ObjectDisposed. Также в коде, приведенном выше, переменная _disposed умышленно сделана приватной. Это заставляет производные типы добавлять собственный флаг завершения, и ошибки, которые могут возникать при освобождении объекта, не распространяются по иерархии наследования. Думаю, что также уместным будет продемонстрировать шаблон для класса наследника:

    ExpandedWrap disabled
      public class DirevedClass : MyClass
          {
              private bool _isDisposed = false;
       
              protected override void Dispose(bool isDisposing)
              {
                  if (_isDisposed)
                      return;
       
                  if (isDisposing)
                  {
                      // освобождаем управляемые ресурсы
                  }
                  // освобождаем неуправляемые ресурсы
       
                  // заметим, что подавление завершителя делегируется базовому типу
                  // при этом производный тип вызывает очистку базового класса
                  base.Dispose(isDisposing);
                  _isDisposed = true;
              }
          }


    До новых встреч. :)
    Сообщение отредактировано: juice -
      juice, зачед, замечательная статья, надо в FAQ :)
      Еще добавил бы, что майкрософты проверяют _isDisposed при обращении к обьекту, и если вызывался Dispose() для него - бросаются исключениями.
        Цитата PIL @
        juice, зачед, замечательная статья, надо в FAQ
        Еще добавил бы, что майкрософты проверяют _isDisposed при обращении к обьекту, и если вызывался Dispose() для него - бросаются исключениями.

        Спасибо. Из текста выше.
        Цитата juice @
        Все общедоступные методы вашего типа не должны выполняться после вызова метода клиентом метода Dispose(), это легко реализовать путем проверки переменной _desposed в теле метода. В случае если объект уже был освобожден, необходимо сгенерировать исключение ObjectDisposed.

        ;)
          аут :) Новый Год виноват, извиняюсь ))
            juice, Хорошая статья :) спасибо.
            Я вот нашел одно "Но". Вот цитатка из МСДН:
            Цитата

            public static void SuppressFinalize (Object obj)

            Parameters
            obj
            The object for which a finalizer must not be called.

            вот из твоего примера:
            Цитата
            GC.SuppressFinalize(true);

            По-идее, вместо true должен быть this...
            Хотя, компилится и выполняется и с "true" :)
              Цитата Miha_Dnepr @
              По-идее, вместо true должен быть this...

              Абсолютно коректное замечание.
              Вот даж на эту тему есть статейка http://msdn2.microsoft.com/en-us/library/ms182269.aspx
              +1.

              Код поправил.
              Сообщение отредактировано: juice -
                Да, кстати, по поводу возникновения ошибки / исключения в методе Dispose(bool).
                ExpandedWrap disabled
                      public class MyClass : IDisposable{
                   
                          private bool _disposed = false;
                   
                          #region IDisposable Members
                   
                          public void Dispose(){
                              Dispose(true);
                              GC.SuppressFinalize(this);
                          }
                   
                          #endregion
                   
                          protected virtual void Dispose(bool isDisposing){
                              if(true == _disposed){
                                  return;
                              }
                   
                              throw new Exception("Ex!");
                   
                              _disposed = true;
                          }
                   
                          ~MyClass(){
                              Dispose(false);
                          }
                   
                      }

                Затем, в вызывающем коде
                ExpandedWrap disabled
                      public partial class Form1 : Form
                      {
                          MyClass c = new MyClass();
                          public Form1()
                          {
                              InitializeComponent();
                          }
                   
                          private void Form1_Load(object sender, EventArgs e)
                          {
                          }
                   
                          private void button1_Click(object sender, EventArgs e)
                          {
                              c = null;
                              GC.Collect();
                          }
                      }


                Угадайте, что происходит? ;) А еще прикольнее, когда не вызываешь явно GC.Collect и не обнуляешь "c = null;", а просто закрыть приложение.
                Как с этим правильно бороться?
                  Цитата Miha_Dnepr @
                  А еще прикольнее, когда не вызываешь явно GC.Collect и не обнуляешь "c = null;", а просто закрыть приложение.

                  Собственно разница небольшая, т.к. после закрытия приложения, освобождается домен, а следовательно и отрабатывает сборщик мусора. Объект имеет finalizer и явно не освобождался, он ставится на завершение в очередь.

                  Цитата Miha_Dnepr @
                  Как с этим правильно бороться?


                  Первое, что приходит в голову это подавить возможные исключения путем заключения кода который бросает исключение в блок try/catch, хотя если код в Dispose может сгенерировать исключение, это сведетельствует о том, что там есть, что то, кроме освобождения ресурсов, что является грубой ошибкой.
                    Мне просто стало интересно, какова физика процесса... Ну, вот, допустим, дошла очередь до сборки мусора. Объекты, не имеющие "деструкторов" (уж, извините, мне это слово привычнее..) уже удалились, и тут, о - чудо! Запускается отдельный поток по сборке "ресурсоемкого" мусора, и в этом потоке вылазит исключение. Что произойдет с дальнейшим процессом сборки, как "поступит" CLR в этом случае? продолжится ли освобождение остальных объектов, с занятыми ресурсами?

                    По-поводу try/catch...
                    ExpandedWrap disabled
                              protected virtual void Dispose(bool isDisposing){
                                  if(true == _disposed){
                                      return;
                                  }
                       
                                  try {
                                      throw new Exception("Ex!");
                                  }
                                  catch{
                                      MessageBox.Show("Опаньки!");
                                      // Ну, или плюс ко всему в лог какой-нибудь записать...
                                  }
                       
                                  _disposed = true;
                              }

                    Решение, впринципе, работает... Думаю, в случаях, когда потенциально может возникнуть ошибка при освобождении ресурсов, таки заключать в блок try/catch "опасный" код.
                    Кстати, а при таком подходе блокируется ли дальнейший процесс сборки? Ну, по-крайней мере, пока юзерь не нажал "Ок" ;)
                      Цитата Miha_Dnepr @
                      Решение, впринципе, работает... Думаю, в случаях, когда потенциально может возникнуть ошибка при освобождении ресурсов, таки заключать в блок try/catch "опасный" код.
                      Кстати, а при таком подходе блокируется ли дальнейший процесс сборки? Ну, по-крайней мере, пока юзерь не нажал "Ок"


                      Нужно стараться, что бы удаление объекта происходило без возникновения исключений. Насамом деле нет "правильного" способа восстановления после исключения во время выполнения очистки в Dispose. Если при очистке объекта возникает исключение, то единственный выход, попытаться попробовать очистить объект заново или согласиться с тем, что часть ресурсов не может быть освобождена. Поэтому в коде очистки нужно проверить все ссылки на объекты, к которым мы будем обращаться внутри Dispose, и что эти ссылки не являются неопределенными, и что методы которые мы вызываем не приводят к возникновению исключений. Это аксиома. При этом исключение в Dispose еще не самый худший вариант, хуже если оно вызвано финализатором, необработанное исключение, возникшее в потоке финализатора, по умолчанию разрушает весь процесс.
                        Цитата juice @
                        необработанное исключение, возникшее в потоке финализатора, по умолчанию разрушает весь процесс.

                        Вот это я и хотел узнать...
                        Теперь вроде все ясно, спасибо.
                          Привет.
                          Конечно, топик довольно старый, но все же такой вопрос: А в переопределенном методе void Dispose(bool) в конце не нужно вызвать Dispose базового класса?
                          Сообщение отредактировано: _Hamlet -
                            Он там вызывается, проскроль последний блок исходного кода для класса потомка.
                            0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                            0 пользователей:


                            Рейтинг@Mail.ru
                            [ Script execution time: 0,0451 ]   [ 15 queries used ]   [ Generated: 28.03.24, 12:10 GMT ]