Название шаблона
Пул объектов (Object pool).
Тип
Порождающий шаблон проектирования (Creational).
Описание
Пул объектов предназначен хранения готовых к использованию объектов. Когда системе требуется новый объект, он запрашивается из Пула, минуя процесс порождения. А после использования возвращается обратно в Пул вместо уничтожения.
Шаблон применяется для повышения производительности, если:
- объекты часто создаются и уничтожаются;
- в системе существует ограниченное количество объектов типа, хранимого в Пуле;
- создание и/или уничтожение объекта являются очень затратными операциями.
Пул объектов может работать как с интерфейсами, так и с конкретными реализациями. Все зависит от архитектуры разрабатываемой системы и решаемых задач.
Можно встретить совместное использование Пула объектов и других порождающих шаблоном. Например, для создания объектов в определенном состоянии может применить Прототип. А при помощи Одиночки – создать единственный экземпляр Пула в системе.
Плохой практикой является сокрытие Пула за другими порождающими шаблонами. Разработчик, использующий такой "гибрид", не ожидает требования возврата объектов от, например, Фабричного метода. А без возврата объектов сам Пул становится бесполезным. В таком случае, правильным решением будет отделить реализацию классов, создающих объекты.
Особенности использования
- Пул ничего не знает о реализации хранимых объектов. Поэтому возвращенный объект считается находящимся в неопределенном состоянии. Для дальнейшего использования его необходимо перевести в начальное состояние (сбросить). Наличие объектов в неопределенном состоянии превращает Пул в "объектную клоаку" (object cesspool).
- Повторное использование может стать причиной утечки конфиденциальной информации. Поэтому необходимо обязательно очищать поля с секретными данными при сбросе, а сами данные – затирать или уничтожать.
- Возможна ситуация, когда в Пуле не останется свободных объектов. В этом случае реакция на запрос может быть следующая:
- увеличение размера пула;
- отказ в выдачи объекта;
- постановка в очередь и ожидание освобождения объекта.
Схожие шаблоны и их отличия
Пул объектов | Прототип | Фабричный метод / Абстрактная фабрика |
Порождает требуемые реализации, но может предоставлять и интерфейсы. | Скрывает реализацию объекта. | Скрывает реализацию объектов. |
Класс. | Метод класса или интерфейса, может включать фабрику. | Метод класса / класс или интерфейс. |
Выдает существующий объект во временное пользование. | Создает копию объекта. | Создает новый объект. |
Реализация шаблона в общем виде
- определяем интерфейс 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:
- Сборка мусора в .NET происходит по поколениям. И чем старшее поколение, тем реже в нем происходит сборка мусора.
- Объекты, за исключением больших, создаются в нулевом поколении. Они переходят в следующее, если выживают после очередной сборки мусора.
- Чем больше будет интенсивность создания объектов, тем чаще будут происходить сборки мусора.
Вернемся к Пулу. Он как раз используется в случаях, когда объекты создаются часто. Это не обязательно происходит в теле цикла, как в примере выше. Порождения могут происходить в разных методах класса или даже разных классов, выполняться в разных потоках и т.д. Важно только то, что они происходят достаточно часто.
Если в такой ситуации использовать обычное создание объектов, то младшее поколение будет накапливать большое количество мусора. Это приведет к увеличению числа запусков сборщика. Кроме того, часть объектов успеют перейти в старшие поколения. В итоге – пусть небольшое, но уменьшение производительности и увеличение расхода памяти.
При корректной работе с Пулом уменьшается число порождаемых объектов. Кроме того, они сами могут быть спроектированы так, чтобы при сбросе состояния избежать повторного создания экземпляров нужных им объектов. Как следствие – уменьшение расхода памяти, мусора, частоты запуска сборщика.