Автор: Sergey Teplyakov

Пред. запись: Шаблонный Метод След. запись: Паттерн Посредник

Данная статья является продолжением обсуждения паттерна Шаблонный Метод, начатого в прошлый раз.

Одной из ключевой идиом языка С++ является идиома RAII Resource Acquisition Is Initialization. Главная ее идея заключается в том, что некоторый ресурс, например, память, дескриптор, сокет и т.п. захватывается в конструкторе и освобождается в деструкторе. А поскольку деструкторы локальных объектов вызываются обязательно, независимо от того, по какой причине управление покидает текущую область видимости, мы получаем полуавтоматическое управление ресурсами.

При этом автоматическое управление применяется не только для ресурсов памяти, дескрипторов, файлов или сокетов, но и для других целей. Так например, автоматический вызов деструктора используется в многопоточных приложениях для реализации критических секций в коде. Для этого используются классы std::mutex, а также класс std::unique_lock, который захватывает блокировку в конструкторе и освобождает в деструкторе:

std::mutex m,int sh, // разделяемые данные// ...void f(){ // ... std::unique_lock lck(m), // операции над разделяемыми данными: sh += 1,}

В управляемых средах, таких как CLR идиома RAII распространена не настолько сильно. Проблемы управления памятью берет на себя сборщик мусора, а за детерминированное освобождение ресурсов отвечают классы, реализующие интерфейс IDisposable, метод Dispose которых затем вызывается в блоке finally. Но поскольку ручное освобождение ресурсов в блоках finally является неудобным, то в большинстве управляемых языков существует синтаксический сахар: using statement в C#, use binding в F# и т.п.

Когда речь заходит о разработке класса, содержащего управляемые или неуправляемые ресурсы, то решение довольно простое: нужно реализовать интерфейс IDisposable, а в случае неуправляемых ресурсов добавить еще и финализатор и реализовать полноценный Disposable Pattern (см. статью Джо Даффи 'Dispose, Finalization, and Resource Management', а также Programming Stuff: Dispose Pattern).

Однако что делать, если мы хотим 'автоматизировать' не управление ресурсами, а упростить работу с критическими секциями и классом ReaderWriterLockSlim, аналогично тому, как это сделано с std::unique_lock в С++?

Одним из вариантов решения является создание нескольких методов расширения, возвращающих IDisposable объект, конструктор которого захватывает блокировку, а метод Dispose ее освобождает:public static class DisposeBasedExtensions{ public static IDisposable UseReadLock( this ReaderWriterLockSlim rwLock) { Contract.Requires(rwLock != null),rwLock.EnterReadLock(), return new LambdaBasedWrapper(rwLock.ExitReadLock), }public static IDisposable UseWriteLock( this ReaderWriterLockSlim rwLock) { Contract.Requires(rwLock != null),rwLock.EnterWriteLock(), return new LambdaBasedWrapper(rwLock.ExitWriteLock), }private class LambdaBasedWrapper : IDisposable { private readonly Action _releaseAction,public LambdaBasedWrapper(Action releaseAction) { _releaseAction = releaseAction, }public void Dispose() { _releaseAction(), } }}

Теперь при наличии общего ресурса (например, Dictionary&lt,int, int&gt, _dictionary) и блокировки (ReaderWriterLockSlim _rwLock) можно будет использовать методы расширения следующим образом:// Используем блокировку на записьusing (_rwLock.UseWriteLock()){ _dictionary[42]++,}// Используем блокировку на чтениеint value,using (_rwLock.UseReadLock()){ value = _dictionary[1],}

С одной стороны такой подход полностью оправдан, поскольку он удобен и безопасен с точки зрения исключений. С другой стороны, тот же Эрик Липперт в аннотированной спецификации языка C# ('The C# Programming Language' by Anders Hejlsberg) предостерегает от подобного использования Disposable-объектов.

8.13 Оператор using.Эрик Липперт. В данной спецификации явно сказано, что суть оператора using заключается в том, чтобы гарантировать захват и своевременное освобождение ресурса. Обычно, это означает существование некоторого неуправляемого ресурса, полученного от операционной системы, такие как дескриптор файла. Очень полезно завершить использование ресурсов как можно раньше, другая программа сможет прочитать файл после того, как вы завершите с ним работу. Но я не рекомендую использовать оператор using для обеспечения инвариантов приложения. Например, иногда можно встретить следующий код:using(new TemporarilyStopReportingErrors() AttemptSomething(),Здесь TemporarilyStopReportingErrors это тип, который в качестве побочного эффекта отключает уведомление об ошибках, а метод Dispose восстанавливает исходное состояние. Я считаю, что эта (довольно распространенная) практика является примером неправильного использования оператора using, побочные эффекты приложения не являются ресурсами, и глобальные побочные эффекты в конструкторе и методе Dispose не кажутся плохой идеей. В этом случае я бы предпочел использовать простой блок try/finally.

Но поскольку использовать каждый раз try/finally не очень удобно, то вместо реализации IDisposable-оболочки, мы можем написать простой вспомогательный метод на основе расмотренного ранее 'локального Шаблонного Метода':public static void WithReadLock(this ReaderWriterLockSlim rwLock, Action action){ try { rwLock.EnterReadLock(), action(), } finally { rwLock.ExitReadLock(), }}

И теперь, при наличии все той же блокировки (_rwLock) и разделяемого ресурса (_dictionary), мы сможем использовать данные методы расширения для потокобезопасной работы:// Используем блокирвку на запись_rwLock.WithWriteLock( () =&gt, { _dictionary[42]++, }),// Используем блокировку на чтениеint value,_rwLock.WithReadLock( () =&gt, { value = _dictionary[1],}),

ПРИМЕЧАНИЕРеализация методов для захваты блокировки на запись или 'обновляемой' (upgradeable) блокировок, аналогичны.

Так что же выбрать?

У каждого из рассмотренных подходов есть свои особенности. Прежде всего, ни один из них не дотягивает до возможностей языка С++, в котором идиома RAII может применяться не только на уровне методов, но и на уровне полей классов. Так, ни один из этих подходов не будет работать, если мы захотим создать класс, который содержит два поля, управляющих разными ресурсами подобным образом.

Но даже в рамках одного метода у этих подходов есть свои достоинства и недостатки.

Так, хотя Эрик Липперт не рекомендует использовать Disposable-объекты и блок using не для освобождения ресурсов, такой подход действительно активно используется, причем не только сторонними разработчиками, но и разработчиками Microsoft. Достаточно вспомнить сценарии использования классов TransactionScope или библиотеку Microsoft Fakes, в которой используется этот же подход для подделки статических и невиртуальных методов с помощью Shims:// Shim-ы могут использоваться только внутри ShimsContext:using (ShimsContext.Create()){// 'Подделываем' DateTime.Now для возврата нужной даты: System.Fakes.ShimDateTime.NowGet = () =>, new DateTime(2001, 1, 1),Assert.That(DateTime.Now.Date.Year, Is.EqualTo(2001)),Assert.That(DateTime.Now.Date.Day, Is.EqualTo(1)),Assert.That(DateTime.Now.Date.Day, Is.EqualTo(1)),} 

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

С другой стороны, подход на основе 'локального Шаблонного Метода' тоже далек от идеала: ведь в этом случае мы создаем анонимные методы, обилие которых вполне может серьезно повлиять на его стоимость во время исполнения. Методы расширения, возвращающие Disposable-объекты тоже реализованы с помощью анонимных методов, но в случае проблем мы всегда сможем легко от них отказаться.

Поэтому если речь идет о критическом к производительности коде, то простой код на основе try/finally будет оптимальным решением. Если же это не performance critical код, то тут есть выбор: 'локальный Шаблонный Метод' или IDisposable-оболочка. И здесь все больше зависит от вашего вкуса и того, насколько конкретная реализация упростит читаемость кода.

Дополнительные ссылки
  • Паттерн Шаблонный Метод
  • Dispose, Finalization, and Resource Management by Joe Duffy
  • Programming Stuff: Dispose Pattern
  • Блок Эрика Липперта
  • The C# Programming Language by Anders Hejlsberg at al
  • Об идиоме RAII и классе ReaderWriterLockSlim
  • Гарантии безопасности исключений
Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).