Порождающие шаблоны: Инициализация при получении ресурса

Название шаблона

Инициализация при получении ресурса (Resource Acquisition Is Initialization или RAII).

Тип

Порождающий шаблон проектирования (Creational).

Описание

Шаблон "Инициализация при получении ресурса" используется для управления доступом к некоему ресурсу. При этом инициализация объекта совмещается с его получением, а уничтожение – с обязательным освобождением.

Реализация шаблона должна гарантировать, что при уничтожении объекта ресурс будет освобожден. Причем не зависимо от того, как закончился блок, в котором он используется: нормально, с ошибкой или по исключению.

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

Реализация шаблона в общем виде

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

Реализация и ее особенности в C#

Как известно, в .NET уничтожение переменной не гарантирует уничтожение самого объекта. Он будет удален из памяти при очередном проходе сборщика мусора. При этом не должно быть других переменных, ссылающихся на него. Предугадать когда это произойдет практически невозможно.

Как следствие, в C# нет деструкторов, как они понимаются, например, в С++ и других языках. Вместо них есть финализаторы (finalizer). Но с ними связан ряд особенностей:

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

В качестве решения можно предложить обернуть часть кода, где используется некий ресурс, в конструкцию try-finally. Тогда в блоке finally можно освободить занятый ресурс. Такой подход гарантирует что это произойдет при любом варианте завершения контролируемого блока. На практике, понимание такого кода несколько усложняется, т.к. сразу не ясна цель использования конструкции try-finally. Да и выглядит, на мой взгляд, такой код не очень опрятно.

Поэтому в C# введен оператор using, который помогает избавиться от указанных выше проблем. Его использование выглядит так:

using (var obj = new SomeObject()) {
    // Do something with 'obj'
}

Здесь есть одно условие – класс SomeObject должен реализовывать интерфейс IDisposable:

public interface IDisposable
{
    void Dispose();
}

Метод Dispose() отвечает за освобождение используемых ресурсов. Он будет вызываться при выходе из контролируемого блока.

Поскольку using это особенность C#, то при компиляции он будет развернут в try-finally:

SomeObject obj = null;
try {
    obj = new SomeObject();
    // Do something with 'obj'
}
finally {
    if (obj != null) {
        obj.Dispose();
    }
}

Таком образом, реализация шаблона "Инициализация при получении ресурса" на C# состоит из:

  • реализации IDisposable и освобождении ресурсов в методе Dispose();
  • использовании конструкции using, которая гарантирует освобождение ресурса при выходе из контролируемого блока.

Но есть еще одна проблема: программист, который будет работать с этим классом, может не использовать using или не вызвать Dispose(). Можно ли учесть эту ситуацию?

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

С учетом всех указанных выше моментов, класс будет выглядеть следующим образом:

public class RAIIObject : IDisposable
{
    public RAIIObject()
    {
        // lock resources
    }

    ~RAIIObject()
    {
        throw new InvalidOperationException("you should use 'using' keyword!")
        // or 
        // this.ReleaseResources();            
    }

    void IDisposable.Dispose()
    {
        this.ReleaseResources();
        GC.SuppressFinalize(this);
    }

    private void ReleaseResources()
    {
        // release resources
    }
}

Комментарии (9) -

На мой взгляд пост не только сильно упрощает механизмы управления памятью в .NET, но и искажает их. Например, объекты, имеющие финализатор помещаются в отдельную очередь - это одна из причин почему финализаторы никогда не должны генерировать исключения! Рекомендую к прочтению http://rsdn.ru/article/dotnet/GCnet.xml
О том, что программист неправильно освобождает ресурсы (забыл вызвать метод Dispose() - зачем кидаться в него исключениями?) могут подсказать сторонние утилиты, такие как Resharper, StyleCop, FoxCop и другие методом анализа исходного кода.

Задача поста не пояснение механизма работы с памятью. Именно по этому не вдаюсь в подробности. тема GC очень обширная и заслуживает отдельного поста. Если будет время завершу скринкаст. Там будут эти подробности.

Отдельная очередь финализатора никак не связана с шаблоном. Поэтому зачем о ней говорить? Нагрузить лишним?

Насчет исключения – у меня встречный вопрос, а почему не стоит кидать исключение? При ответе прошу учесть, что я отметил, что явно место ошибки определить это не даст. Кроме того, было предложено 2 варианта (исключение или молча обработать ситуацию). Какой вариант использовать – надо решать уже от типа ресурса. Что-то можно простить и освободить в тихую. А если ресурс часто используемый, да еще "только в одни руки"? В этом случае я предпочту исключение. Т.к. сторонние средства это хорошо, но кто даст гарантию что они есть, используются и используются правильно.

Интересно узнать ваше мнение.

Основная мысль моего комментария - финализаторы никогда не должны генерировать исключения. Что будет с остальными объектами в очереди финализации, если один из них кинет исключение при финализации, как в одном из ваших вариантов? Кроме того, финализаторы могут вообще не быть вызваны, в этом случае исключения никто не увидит.
Согласен, что вариант не единственный, но для ручного освобождения существует свой шаблон. Например, здесь msdn.microsoft.com/.../b1yfkh5e(v=VS.100).aspx
Честно говоря, про "только в одни руки" не понял. Вы про синхронизацию доступа? Тогда Monitor или Mutex. Или про глобальный объект? Тогда Singleton. Или про повторное открытие? Тогда исключение должно быть в методе инициализации этого подключения.

osmirnov :
Что будет с остальными объектами в очереди финализации, если один из них кинет исключение при финализации, как в одном из ваших вариантов?

А не все ли равно уже? Главное что это произойдет скорое даже до начала тестирования.

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

Вот, это я и добиваюсь. Если все написать правильно (т.е. использовать using как задумано автором класса), то никаких исключений не будет, ибо GC.SuppressFinalize(this);

На мой взгляд, если класс задумывается разработчиком для работе в using то уж будьте добры его так и использовать. В этом случае никаких исключений не будет. Взялись вызывать Dispose руками? Будьте добры предусмотреть все варианты освобождения ресурса.

И еще раз подчеркну – я не утверждаю что всегда надо делать именно так. Но если надо освобождать ресурс после доступа (и выхода из блока), то значит надо. Не просто так это придумано. Хотя, может это отголоски С++ного прошлого. Smile

Или про повторное открытие? Тогда исключение должно быть в методе инициализации этого подключения.

Да, именно про эксклюзивный доступ. Согласен, должно быть исключение при получении доступа. Но возникнет вопрос – а кто держит ресурс. С другой стороны, если в классе есть что-то что поможет определить его экземпляр, это можно вставить в текст исключения.

А не все ли равно уже? Главное что это произойдет скорое даже до начала тестирования.
Например, могут остаться незакрытыми неуправляемые ресурсы, как следствие это прямые утечки памяти. Если тесты покрывают код на 100%, я с вами полностью согласен.
Когда я говорил
Кроме того, финализаторы могут вообще не быть вызваны, в этом случае исключения никто не увидит.
Я имел ввиду не вызов GC.SuppressFinalize(this), а исключительную ситуацию, например, если по какой-либо причине не будет вызван завершающий код процесса.
Это редкая, но потенциально возможная ситуация.
Для себя взял за правило - перед использованием нового класса, смотреть реализует ли он IDisposable (делаю это через окно Code Definition Window).

osmirnov :
Например, могут остаться незакрытыми неуправляемые ресурсы, как следствие это прямые утечки памяти. Если тесты покрывают код на 100%, я с вами полностью согласен.

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

Я имел ввиду не вызов GC.SuppressFinalize(this), а исключительную ситуацию, например, если по какой-либо причине не будет вызван завершающий код процесса. Это редкая, но потенциально возможная ситуация.

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

Для себя взял за правило - перед использованием нового класса, смотреть реализует ли он IDisposable (делаю это через окно Code Definition Window).

Кратко: +1 Smile

Nick Sergeev 25.11.2010 18:40:33

Если единственная цель исключения предупредить разработчика, то может быть логичнее было бы использовать Debug.Assert?

Исключение лучше, учитывая что подведение приложения в момент выброса не определено. Например, если приложение в этот момент завершается, то содержимое окна Assert можно просто не успеть заметить (или даже само окно).

Богдан 07.08.2014 16:12:20

Очень просто и доходчиво описано и про перестраховку в finaly упомянуто, и про GC.SuppressFinalize(this). Все что Microsoft рекомендует на MSDN только просто и понятно. Про работу GC все упрощено сильно , но ведь тема статьи не GC. Исключение в finaly это плохо если оно вылетает не всегда и может быть не замечено, а явный выброс исключения заставит программиста решить проблему освобождения ресурсов.  Автор молодец простые и полезные статьи, пиши и дальше.
Вот маленькая деталь:
SomeObject obj = new SomeObject();
try {
   // Не здесь
    // Do something with 'obj'
}
finally {
    if (obj != null) {
        obj.Dispose();
    }
}
Объект будет создан вне блока try, иначе его ссылка будет не видна в блоке finally .

Добавить комментарий