Andrey on .NET | Порождающие шаблоны: Пул объектов (Object pool)

Порождающие шаблоны: Пул объектов (Object pool)

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

Пул объектов (Object pool).

Тип

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

Описание

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

Шаблон применяется для повышения производительности, если:

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

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

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

Плохой практикой является сокрытие Пула за другими порождающими шаблонами. Разработчик, использующий такой "гибрид", не ожидает требования возврата объектов от, например, Фабричного метода. А без возврата объектов сам Пул становится бесполезным. В таком случае, правильным решением будет отделить реализацию классов, создающих объекты.

Особенности использования

  1. Пул ничего не знает о реализации хранимых объектов. Поэтому возвращенный объект считается находящимся в неопределенном состоянии. Для дальнейшего использования его необходимо перевести в начальное состояние (сбросить). Наличие объектов в неопределенном состоянии превращает Пул в "объектную клоаку" (object cesspool).
  2. Повторное использование может стать причиной утечки конфиденциальной информации. Поэтому необходимо обязательно очищать поля с секретными данными при сбросе, а сами данные – затирать или уничтожать.
  3. Возможна ситуация, когда в Пуле не останется свободных объектов. В этом случае реакция на запрос может быть следующая:
    • увеличение размера пула;
    • отказ в выдачи объекта;
    • постановка в очередь и ожидание освобождения объекта.

Схожие шаблоны и их отличия

Пул объектов Прототип Фабричный метод / Абстрактная фабрика
Порождает требуемые реализации, но может предоставлять и интерфейсы. Скрывает реал��зацию объекта. Скрывает реализацию объектов.
Класс. Метод класса или интерфейса, может включать фабрику. Метод класса / класс или интерфейс.
Выдает существующий объект во временное пользование. Создает копию объекта. Создает новый объект.

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

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

Примеры реализации

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

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

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

/// <summary>The poolable object interface</summary>
public interface IPoolable
{
    /// <summary>Resets the object's state.</summary>
    void ResetState();
}

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

/// <summary>The pool object creator interface.</summary>
/// <typeparam name="T">Type of the objects to create.</typeparam>
public interface IPoolObjectCreator<T>
{
    /// <summary>Creates new object for a pool.</summary>
    /// <returns>The object.</returns>
    T Create();
}

Сразу реализуем данный интерфейс в виде generic класса для создания с экземпляров с помощью конструктора без параметров:

public class DefaultObjectCreator<T> : IPoolObjectCreator<T> where T : class, new()
{
    T IPoolObjectCreator<T>.Create()
    {
        return new T();
    }
}

Перейдем к созданию непосредственно Пула. В качестве контейнера объектов используем класс ConcurrentBag, реализованный в .NET4. Его важными особенностями являются потокобезопасность, а так же быстрые операции добавления и удаления объектов. Для ранних версий .NET можно использовать, например, класс ArrayList. Но в этом случае необходимо самостоятельно позаботиться о потокобезопасности.

Поскольку размер пула не фиксированный, то его метод GetObject() всегда возвращает объект. Это может быть объект взятый из контейнера или, если он пустой, созданный.

Метод ReturnObject() помещает объект обратно в контейнер. При этом осуществляется сброс его состояния. Кроме того, переменной, содержавшей ссылку на него, присваивается значение null. Таком образом, объект может находиться или в контейнере или вне него. К сожалению средствами Пула не возможно гарантировать возврат объекта и отсутствие дополнительных ссылок на него. Все это оставляем "на совести" клиентского кода. В данной реализации, если объект не будет возвращён, то через какое-то время он будет просто уничтожен сборщиком мусора.

Свойство Count показывает сколько объектов в данный момент находится в пуле.

Полный код класса, реализующего шаблон "Пул объектов", приведен ниже:

public class ObjectPool<T> where T : class, IPoolable
{
    /// <summary>Object container. ConcurrentBag is tread-safe class.</summary>
    private readonly ConcurrentBag<T> _container = new ConcurrentBag<T>();

    /// <summary>Object creator interface.</summary>
    private readonly IPoolObjectCreator<T> _objectCreator;

    /// <summary>Total instances.</summary>
    public int Count { get { return this._container.Count; } }

    /// <summary>
    /// Initializes a new instance of the <see cref="T:ObjectPool"/> class.
    /// </summary>
    /// <param name="creator">Interface of the object creator. It can't be null.</param>
    public ObjectPool(IPoolObjectCreator<T> creator)
    {
        if (creator == null) {
            throw new ArgumentNullException("creator can't be null");
        }

        this._objectCreator = creator;
    }

    /// <summary>Gets an object from the pool.</summary>
    /// <returns>An object.</returns>
    public T GetObject()
    {
        T obj;
        if (this._container.TryTake(out obj)) {
            return obj;
        }

        return this._objectCreator.Create();
    }

    /// <summary>Returns the specified object to the pool.</summary>
    /// <param name="obj">The object to return.</param>
    public void ReturnObject (ref T obj) 
    {
        obj.ResetState();
        this._container.Add(obj);
        obj = null;
    }
}

Проведем небольшой тест. Для этого напишем демонстрационный класс и класс, для его создания.

public class TestObject : IPoolable
{
    public int Index { get; set; }

    public TestObject()
    {
        Console.WriteLine("TestObject constructor.");
        this.Index = -1; 
    }

    void IPoolable.ResetState() 
    {
        this.Index = -1;
    }
}

Как видно из исходного кода, каждый вызов конструктора будет выводить уведомление на консоль.

Для примера, сравним два варианта цикла:

public static void Test()
{
    var pool = new ObjectPool<TestObject>(new DefaultObjectCreator<TestObject>());


    Console.WriteLine("1) Using 'new' keyword ...");
    for (int i = 0; i < 10; i++) {
        TestObject obj = new TestObject();
        // Do something with the test object
        obj.Index = i;
    }

    Console.WriteLine("2) Using Object pool ...");
    for (int i = 0; i < 10; i++) {
        TestObject obj = pool.GetObject();
        // Do something with the test object
        obj.Index = i;
        pool.ReturnObject(ref obj);
    }
}

Если запустить данный код, то будет выведено 10 уведомлений о срабатывании конструктора для цикла с ключевым словом new и всего лишь одно для варианта с Пулом. Разница будет больше, если увеличить число повторений тела цикла до 100 и более. Явный выигрыш по размеру используемой памяти.

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

Обратите внимание, что код получает объект из Пула таким, каким бы он был после вызова конструктора. Это обеспечивается сбросом состояния.

Особенности реализации в .NET и C#

Для начала вспомним некоторые факты о работе с памятью в .NET:

  1. Сборка мусора в .NET происходит по поколениям. И чем старшее поколение, тем реже в нем происходит сборка мусора.
  2. Объекты, за исключением больших, создаются в нулевом поколении. Они переходят в следующее, если выживают после очередной сборки мусора.
  3. Чем больше будет интенсивность создания объектов, тем чаще будут происходить сборки мусора.

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

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

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

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

Добрый день. Честно говоря, немного удивил ваш пост, т.к. предидущие были по GoF - шаблонам. Небольшой bug fix - метод PushBack() замените на ReturnObject().
Вопросы:
1. Какое отличие от GoF'овского шаблона Flyweight?
2. В чём смысл оставлять открытым конструктор TestObject, если это потенциально затратная операция?
Замечания:
1. Мне кажется лишним навязывание объекту знаний о pool через интерфейс IPoolable. В качестве замены предлагаю для сброса состояние, сохранить начальное состояние, используя шаблон Memento и затем восстановить;
2. Весь пост ждал, пока вы проведете аналогию с ThreadPool ))

День добрый. Обещания рассказывать только про GoF шаблоны я не давал, так что руки не связаны. Smile

Теперь ответы:

1. Это конечно лучше упомянуть позже (при описании Flyweight), но раз спросили – отвечу. На мой взгляд, отличие заключается в том, что:
- Flyweight кое-что знает о хранимых объектах, т.к. может подбирать нужный по параметрам. В этом его поведение расширяет Абстрактную Фабрику. Пул абсолютно безразличен к сути хранимых объектов.
- Объект, выданный Flyweight может использоваться в разных местах одновременно. Пул нацелен на выдачу по принципу "один объект в одни руки".

2. Шаблон не обязательно используется при затратном конструкторе (тут может даже лучше подойдет Прототип). Все же основное применение его при большом числе порождаемых объектов.

Теперь по замечаниям:

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

Ну и еще один момент – добавление шаблонов (особенно о которых не было еще рассказано), внесет дополнительную путаницу и усложнит понимание.

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

>>Свойство Title обнуляется
--
Наверно свойство Index. Спс за статьи.

@ Leonid: В первой версии примера был Title. Smile

Евгений Веприков 28.10.2010 7:24:10

>объекты часто создаются и уничтожаются;
И флаг им в руки, в .NET память выделяется быстро, освобождается быстро и сборщик мусора работает быстро, действительно быстро. В крайнем случае лучше потратить пару тысяч на усиление оборудования чем пятьдесят на оптимизацию кода. Не стали ведь для ASP.NET создавать пулов.
>в системе существует ограниченное количество объектов типа, хранимого в Пуле;
Случай когда существует ограничение по используемым лицензиям стороннего ПО от построения сервера приложений как правило.
>создание и/или уничтожение объекта являются очень затратными операциями.
Жуткие COM объекты от контор типа 1С


В последних двух случаях прекрасно справляется ObjectPoolingAttribute.

P.S. ИХМО применение всех этих патернов наносит больше вреда, нежели пользы. Ибо, хотя бы в данном случае, как минимум требуется дополнительное исследование, использовать пул или нет и предварительное исследование, на вопрос проведения дополнительного исследования.
Да и нужно понимать всегда, как реализовать вставшую задачу, а не пытаться применить к решению все возможные знания о потернах.

Евгений Веприков :
И флаг им в руки, в .NET память выделяется быстро, освобождается быстро и сборщик мусора работает быстро, действительно быстро.

Отчасти согласен. Отчасти потому, что для .NET4. И потому, что "быстро" надо указывать относительно чего и для какой задачи. К тому же это не повод увеличивать расход оперативки.

В крайнем случае лучше потратить пару тысяч на усиление оборудования чем пятьдесят на оптимизацию кода.

Далеко не все пишут решения для внутреннего использования. Кроме того, компьютеров в организации может быть больше 10 Smile И более того, .NET вполне можно использовать и на рынке программ для домашних пользователей. Вы и им предложите обновить железо (для работы утилиты за $14.99 купить на $1 000 апгрейдов)?

Случай когда существует ограничение по используемым лицензиям стороннего ПО от построения сервера приложений как правило.

Не обязательно только в этом случае. Просто приложению может не требоваться более какого-то числа экземпляра объектов одновременно.

P.S. ИХМО применение всех этих патернов наносит больше вреда, нежели пользы.

Вот тут вы не правы. Аргументация ниже.

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

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

Кроме того, знание и понимание шаблонов дает возможность быстрее искать решения. Т.е. "да вот это точно 1 в 1 ситуация с Абстрактной фабрикой" – и я уже знаю что делать дальше. Или "тут нужно расширить функциональность класса, но я не могу применить наследование", да это же Декоратор. Т.е. я уже знаю что делать дальше. Кроме того, если я буду разбираться в чьем-то коде, где правильно использованы шаблоны, то понять суть класса (даже по названию) можно быстрее.

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

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

А как нибудь организовать обмен сообщениями между объектами в пуле можно?

@ 2king2: А в чем именно сложность?

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