Подтвердить что ты не робот

Реализация шаблона пула объектов С#

Есть ли у кого-нибудь хороший ресурс по реализации стратегии пула общих объектов для ограниченного ресурса в духе объединения каналов Sql? (т.е. будет полностью реализовано, что он является потокобезопасным).

Чтобы следить за запросом @Aaronaught для разъяснения, использование пула для запросов на балансировку нагрузки для внешней службы. Положить его в сценарий, который, вероятно, будет легче сразу понять, в отличие от моего прямого подхода. У меня есть объект сеанса, который работает аналогично объекту ISession из NHibernate. Каждый уникальный сеанс связывает его с базой данных. В настоящее время у меня есть один длинный сеансовый объект, и я сталкиваюсь с проблемами, когда мой поставщик услуг снижает ставку за использование этого отдельного сеанса.

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

Надеюсь, этот фон предлагает некоторую ценность, но для прямого ответа на некоторые из ваших вопросов:

Q: Являются ли объекты дорогими для создания?
A: Нет объектов - пул ограниченных ресурсов

Q: Будут ли они приобретены/выпущены очень часто?
A: Да, еще раз можно подумать о NHibernate ISessions, где 1 обычно приобретается и выпускается в течение всего запроса каждой страницы.

Q: Будет ли простой доступ для первого приема достаточным или вам нужно что-то более интеллектуальное, т.е. это предотвратит голодание?
A:. Простое распределение типа циклического типа было бы достаточным, голодом я предполагаю, что вы имеете в виду, если нет доступных сеансов, которые вызывающие блокируются в ожидании релизов. Это не применимо, так как сеансы могут использоваться разными абонентами. Моя цель - распределить использование на нескольких сеансах, а не на один сеанс.

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

Q: Как насчет таких вещей, как приоритеты, ленивые против нетерпеливой загрузки и т.д.? A: Нет приоритетов, для простоты просто предположим, что я создам пул доступных объектов при создании самого пула.

4b9b3361

Ответ 1

Объединение объектов в .NET Core

В dotnet core реализована реализация пула объектов, добавленного в библиотеку базового класса (BCL). Вы можете прочитать исходную версию GitHub здесь и просмотреть код для System.Buffers. В настоящее время ArrayPool является единственным доступным типом и используется для объединения массивов. Здесь есть хорошая запись в блоге здесь.

namespace System.Buffers
{
    public abstract class ArrayPool<T>
    {
        public static ArrayPool<T> Shared { get; internal set; }

        public static ArrayPool<T> Create(int maxBufferSize = <number>, int numberOfBuffers = <number>);

        public T[] Rent(int size);

        public T[] Enlarge(T[] buffer, int newSize, bool clearBuffer = false);

        public void Return(T[] buffer, bool clearBuffer = false);
    }
}

Пример его использования можно увидеть в ASP.NET Core. Поскольку он находится в ядре dotnet core BCL, ASP.NET Core может совместно использовать пул объектов с другими объектами, такими как сериализатор Newtonsoft.Json JSON. Вы можете прочитать этот блог для получения дополнительной информации о том, как Newtonsoft.Json делает это.

Объединение объектов в Microsoft Roslyn С# Compiler

Новый компилятор Microsoft Roslyn С# содержит тип ObjectPool, который используется для объединения часто используемых объектов, которые обычно получают новые и сбор мусора очень часто. Это уменьшает количество и размер операций по сбору мусора, которые должны произойти. Существует несколько различных подэлементов, использующих ObjectPool (см. Почему в Roslyn существует так много реализаций объединения объектов?).

1 - SharedPools - Сохраняет пул из 20 объектов или 100, если используется BigDefault.

// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
    // Do something with pooledObject.Object
}

// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);

// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][4] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
    // Do something with list
}
finally
{
    SharedPools.Default<List<Foo>>().Free(list);
}

2 - ListPool и StringBuilderPool - Не строго отдельные реализации, а оболочки вокруг реализации SharedPools, показанные выше, специально для List и StringBuilder. Таким образом, это повторно использует пул объектов, хранящихся в SharedPools.

// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);

// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
    // Do something with stringBuilder
}
finally
{
    StringBuilderPool.Free(stringBuilder);
}

3 - PooledDictionary и PooledHashSet - Они используют ObjectPool напрямую и имеют совершенно отдельный пул объектов. Сохраняет пул из 128 объектов.

// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();

// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
    // Do something with hashSet.
}
finally
{
    hashSet.Free();
}

Microsoft.IO.RecyclableMemoryStream

Эта библиотека обеспечивает объединение для объектов MemoryStream. Это замена для System.IO.MemoryStream. Он имеет точно такую ​​же семантику. Он был разработан инженерами Bing. Прочитайте сообщение в блоге здесь или посмотрите код GitHub.

var sourceBuffer = new byte[]{0,1,2,3,4,5,6,7}; 
var manager = new RecyclableMemoryStreamManager(); 
using (var stream = manager.GetStream()) 
{ 
    stream.Write(sourceBuffer, 0, sourceBuffer.Length); 
}

Обратите внимание, что RecyclableMemoryStreamManager должен быть объявлен один раз, и он будет работать для всего процесса - это пул. Если вы хотите, вы можете использовать несколько пулов.

Ответ 2

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

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

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

  • Стратегия загрузки ресурсов - нетерпеливая или ленивая;
  • Механизм загрузки ресурсов - как его построить;
  • Стратегия доступа - вы упоминаете "круглый робин", который не так прост, как кажется; эта реализация может использовать циклический буфер, который подобен, но не идеален, потому что пул не имеет контроля над тем, когда ресурсы фактически возвращены. Другие варианты: FIFO и LIFO; FIFO будет иметь больше шаблонов с произвольным доступом, но LIFO значительно упрощает реализацию стратегии наименее используемого освобождения (которая, как вы сказали, выходит за рамки, но все равно стоит упомянуть).

Для механизма загрузки ресурсов .NET уже дает нам чистую абстракцию - делегаты.

private Func<Pool<T>, T> factory;

Передайте это через конструктор пула, и мы закончим с этим. Использование общего типа с ограничением new() тоже работает, но это более гибко.


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

public class Pool<T> : IDisposable
{
    // Other code - we'll come back to this

    interface IItemStore
    {
        T Fetch();
        void Store(T item);
        int Count { get; }
    }
}

Концепция здесь проста: мы разрешаем общедоступному классу Pool обрабатывать общие проблемы, такие как безопасность потоков, но используем разные "хранилища элементов" для каждого шаблона доступа. LIFO легко представлен стеком, FIFO - это очередь, и я использовал не очень оптимизированную, но, вероятно, адекватную реализацию циклического буфера, используя указатель List<T> и указатель для приближения шаблона доступа с циклическим циклом.

Все классы ниже являются внутренними классами Pool<T> - это был выбор стиля, но поскольку они действительно не предназначены для использования вне Pool, это имеет наибольший смысл.

    class QueueStore : Queue<T>, IItemStore
    {
        public QueueStore(int capacity) : base(capacity)
        {
        }

        public T Fetch()
        {
            return Dequeue();
        }

        public void Store(T item)
        {
            Enqueue(item);
        }
    }

    class StackStore : Stack<T>, IItemStore
    {
        public StackStore(int capacity) : base(capacity)
        {
        }

        public T Fetch()
        {
            return Pop();
        }

        public void Store(T item)
        {
            Push(item);
        }
    }

Это очевидные - стек и очередь. Я не думаю, что они действительно оправдывают много объяснений. Круговой буфер немного сложнее:

    class CircularStore : IItemStore
    {
        private List<Slot> slots;
        private int freeSlotCount;
        private int position = -1;

        public CircularStore(int capacity)
        {
            slots = new List<Slot>(capacity);
        }

        public T Fetch()
        {
            if (Count == 0)
                throw new InvalidOperationException("The buffer is empty.");

            int startPosition = position;
            do
            {
                Advance();
                Slot slot = slots[position];
                if (!slot.IsInUse)
                {
                    slot.IsInUse = true;
                    --freeSlotCount;
                    return slot.Item;
                }
            } while (startPosition != position);
            throw new InvalidOperationException("No free slots.");
        }

        public void Store(T item)
        {
            Slot slot = slots.Find(s => object.Equals(s.Item, item));
            if (slot == null)
            {
                slot = new Slot(item);
                slots.Add(slot);
            }
            slot.IsInUse = false;
            ++freeSlotCount;
        }

        public int Count
        {
            get { return freeSlotCount; }
        }

        private void Advance()
        {
            position = (position + 1) % slots.Count;
        }

        class Slot
        {
            public Slot(T item)
            {
                this.Item = item;
            }

            public T Item { get; private set; }
            public bool IsInUse { get; set; }
        }
    }

Я мог бы выбрать несколько разных подходов, но суть в том, что ресурсы должны быть доступны в том же порядке, в котором они были созданы, а это значит, что мы должны поддерживать ссылки на них, но отмечаем их как "используемые", (или нет). В худшем случае доступен только один слот, и для каждой выборки требуется полная итерация буфера. Это плохо, если у вас есть сотни ресурсов, собранных и приобретающих и выпуская их несколько раз в секунду; на самом деле это не проблема для пула из 5-10 предметов, и в типичном случае, когда ресурсы легко используются, он должен только продвигать один или два слота.

Помните, что эти классы являются частными внутренними классами - поэтому им не требуется много проверки ошибок, сам пул ограничивает доступ к ним.

Выполните перечисление и метод factory, и мы закончили с этой частью:

// Outside the pool
public enum AccessMode { FIFO, LIFO, Circular };

    private IItemStore itemStore;

    // Inside the Pool
    private IItemStore CreateItemStore(AccessMode mode, int capacity)
    {
        switch (mode)
        {
            case AccessMode.FIFO:
                return new QueueStore(capacity);
            case AccessMode.LIFO:
                return new StackStore(capacity);
            default:
                Debug.Assert(mode == AccessMode.Circular,
                    "Invalid AccessMode in CreateItemStore");
                return new CircularStore(capacity);
        }
    }

Следующая проблема для решения - стратегия загрузки. Я определил три типа:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Первые два должны быть само собой разумеющимися; третий - своего рода гибрид, он лениво загружает ресурсы, но на самом деле не запускает повторное использование любых ресурсов, пока пул не будет заполнен. Это было бы неплохим компромиссом, если вы хотите, чтобы пул был заполнен (что звучит так, как вы), но хочет отложить расходы на их создание до первого доступа (то есть улучшить время запуска).

Методы загрузки на самом деле не слишком сложны, теперь, когда у нас есть абстракция магазина:

    private int size;
    private int count;

    private T AcquireEager()
    {
        lock (itemStore)
        {
            return itemStore.Fetch();
        }
    }

    private T AcquireLazy()
    {
        lock (itemStore)
        {
            if (itemStore.Count > 0)
            {
                return itemStore.Fetch();
            }
        }
        Interlocked.Increment(ref count);
        return factory(this);
    }

    private T AcquireLazyExpanding()
    {
        bool shouldExpand = false;
        if (count < size)
        {
            int newCount = Interlocked.Increment(ref count);
            if (newCount <= size)
            {
                shouldExpand = true;
            }
            else
            {
                // Another thread took the last spot - use the store instead
                Interlocked.Decrement(ref count);
            }
        }
        if (shouldExpand)
        {
            return factory(this);
        }
        else
        {
            lock (itemStore)
            {
                return itemStore.Fetch();
            }
        }
    }

    private void PreloadItems()
    {
        for (int i = 0; i < size; i++)
        {
            T item = factory(this);
            itemStore.Store(item);
        }
        count = size;
    }

Поля size и count выше относятся к максимальному размеру пула и общему количеству ресурсов, принадлежащих пулу (но не обязательно доступны), соответственно. AcquireEager является самым простым, он предполагает, что элемент уже находится в хранилище - эти элементы будут предварительно загружены при построении, т.е. в показанном последним методе PreloadItems.

AcquireLazy проверяет, есть ли в пуле свободные элементы, а если нет, то создается новый. AcquireLazyExpanding создаст новый ресурс, пока пул еще не достигнет своего целевого размера. Я попытался оптимизировать это, чтобы свести к минимуму блокировку, и я надеюсь, что не ошибался (я тестировал это в многопоточных условиях, но явно не исчерпывающе).

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


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

    private bool isDisposed;
    private Func<Pool<T>, T> factory;
    private LoadingMode loadingMode;
    private IItemStore itemStore;
    private int size;
    private int count;
    private Semaphore sync;

Отвечая на вопрос, который я замалчивал в последнем абзаце - как мы ограничиваем общее количество созданных ресурсов, выясняется, что у .NET уже есть отличный инструмент для этого, он называется Semaphore, и он специально разработан, чтобы разрешить фиксированное число потоков доступа к ресурсу (в этом случае "ресурс" является внутренним хранилищем элементов). Поскольку мы не реализуем полнофункциональную очередь производителей/потребителей, это вполне соответствует нашим потребностям.

Конструктор выглядит так:

    public Pool(int size, Func<Pool<T>, T> factory,
        LoadingMode loadingMode, AccessMode accessMode)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", size,
                "Argument 'size' must be greater than zero.");
        if (factory == null)
            throw new ArgumentNullException("factory");

        this.size = size;
        this.factory = factory;
        sync = new Semaphore(size, size);
        this.loadingMode = loadingMode;
        this.itemStore = CreateItemStore(accessMode, size);
        if (loadingMode == LoadingMode.Eager)
        {
            PreloadItems();
        }
    }

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

Так как почти все было чисто абстрагировано к настоящему времени, фактические методы Acquire и Release действительно очень просты:

    public T Acquire()
    {
        sync.WaitOne();
        switch (loadingMode)
        {
            case LoadingMode.Eager:
                return AcquireEager();
            case LoadingMode.Lazy:
                return AcquireLazy();
            default:
                Debug.Assert(loadingMode == LoadingMode.LazyExpanding,
                    "Unknown LoadingMode encountered in Acquire method.");
                return AcquireLazyExpanding();
        }
    }

    public void Release(T item)
    {
        lock (itemStore)
        {
            itemStore.Store(item);
        }
        sync.Release();
    }

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

И последнее, но не менее важное: там очистка:

    public void Dispose()
    {
        if (isDisposed)
        {
            return;
        }
        isDisposed = true;
        if (typeof(IDisposable).IsAssignableFrom(typeof(T)))
        {
            lock (itemStore)
            {
                while (itemStore.Count > 0)
                {
                    IDisposable disposable = (IDisposable)itemStore.Fetch();
                    disposable.Dispose();
                }
            }
        }
        sync.Close();
    }

    public bool IsDisposed
    {
        get { return isDisposed; }
    }

Цель этого свойства IsDisposed станет понятна через мгновение. Весь основной метод Dispose действительно представляет собой удаление фактических пустых элементов, если они реализуют IDisposable.


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

Скажем, мы начнем со следующего простого интерфейса/класса:

public interface IFoo : IDisposable
{
    void Test();
}

public class Foo : IFoo
{
    private static int count = 0;

    private int num;

    public Foo()
    {
        num = Interlocked.Increment(ref count);
    }

    public void Dispose()
    {
        Console.WriteLine("Goodbye from Foo #{0}", num);
    }

    public void Test()
    {
        Console.WriteLine("Hello from Foo #{0}", num);
    }
}

Здесь наш притворный ресурс Foo, который реализует IFoo и имеет некоторый шаблонный код для генерации уникальных идентификаторов. Мы создаем еще один специальный, объединенный объект:

public class PooledFoo : IFoo
{
    private Foo internalFoo;
    private Pool<IFoo> pool;

    public PooledFoo(Pool<IFoo> pool)
    {
        if (pool == null)
            throw new ArgumentNullException("pool");

        this.pool = pool;
        this.internalFoo = new Foo();
    }

    public void Dispose()
    {
        if (pool.IsDisposed)
        {
            internalFoo.Dispose();
        }
        else
        {
            pool.Release(this);
        }
    }

    public void Test()
    {
        internalFoo.Test();
    }
}

Это просто проверяет все "реальные" методы на его внутренний IFoo (мы могли бы сделать это с помощью библиотеки Dynamic Proxy, например Castle, но я не буду в этом разбираться). Он также поддерживает ссылку на Pool, которая ее создает, так что, когда мы Dispose этот объект, он автоматически возвращается в пул. За исключением, когда пул уже был удален - это означает, что мы находимся в режиме "очистки", и в этом случае он фактически очищает внутренний ресурс.


Используя вышеприведенный подход, мы получаем такой код:

// Create the pool early
Pool<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
    LoadingMode.Lazy, AccessMode.Circular);

// Sometime later on...
using (IFoo foo = pool.Acquire())
{
    foo.Test();
}

Это очень хорошая вещь, чтобы быть в состоянии сделать. Это означает, что код, который использует IFoo (в отличие от кода, который его создает), фактически не должен знать о пуле. Вы можете даже вводить IFoo объекты с помощью вашей любимой библиотеки DI и Pool<T> в качестве поставщика /factory.


Я положил полный код на PasteBin для вашего удовольствия от копирования и вставки. Там также есть короткая тестовая программа, которую вы можете использовать, чтобы играть с различными режимами загрузки/доступа и многопоточными условиями, чтобы убедиться в том, что это поточно-безопасные и не багги.

Сообщите мне, есть ли у вас какие-либо вопросы или опасения по поводу этого.

Ответ 3

Что-то вроде этого может удовлетворить ваши потребности.

/// <summary>
/// Represents a pool of objects with a size limit.
/// </summary>
/// <typeparam name="T">The type of object in the pool.</typeparam>
public sealed class ObjectPool<T> : IDisposable
    where T : new()
{
    private readonly int size;
    private readonly object locker;
    private readonly Queue<T> queue;
    private int count;


    /// <summary>
    /// Initializes a new instance of the ObjectPool class.
    /// </summary>
    /// <param name="size">The size of the object pool.</param>
    public ObjectPool(int size)
    {
        if (size <= 0)
        {
            const string message = "The size of the pool must be greater than zero.";
            throw new ArgumentOutOfRangeException("size", size, message);
        }

        this.size = size;
        locker = new object();
        queue = new Queue<T>();
    }


    /// <summary>
    /// Retrieves an item from the pool. 
    /// </summary>
    /// <returns>The item retrieved from the pool.</returns>
    public T Get()
    {
        lock (locker)
        {
            if (queue.Count > 0)
            {
                return queue.Dequeue();
            }

            count++;
            return new T();
        }
    }

    /// <summary>
    /// Places an item in the pool.
    /// </summary>
    /// <param name="item">The item to place to the pool.</param>
    public void Put(T item)
    {
        lock (locker)
        {
            if (count < size)
            {
                queue.Enqueue(item);
            }
            else
            {
                using (item as IDisposable)
                {
                    count--;
                }
            }
        }
    }

    /// <summary>
    /// Disposes of items in the pool that implement IDisposable.
    /// </summary>
    public void Dispose()
    {
        lock (locker)
        {
            count = 0;
            while (queue.Count > 0)
            {
                using (queue.Dequeue() as IDisposable)
                {

                }
            }
        }
    }
}

Пример использования

public class ThisObject
{
    private readonly ObjectPool<That> pool = new ObjectPool<That>(100);

    public void ThisMethod()
    {
        var that = pool.Get();

        try
        { 
            // Use that ....
        }
        finally
        {
            pool.Put(that);
        }
    }
}

Ответ 4

В тот же день Microsoft предоставила инфраструктуру через Microsoft Transaction Server (MTS), а затем COM + для создания пула объектов для COM-объектов. Эта функциональность была перенесена в System.EnterpriseServices в .NET Framework и теперь в Windows Communication Foundation.

Объединение объектов в WCF

Эта статья от .NET 1.1, но все равно должна применяться в текущих версиях Framework (хотя WCF является предпочтительным методом).

Объединение объектов .NET

Ответ 5

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

  • Измените sync.WaitOne() на sync.WaitOne(timeout) и выведите таймаут в качестве параметра в методе Acquire(int timeout). Это также потребовало бы обработки условия, когда поток истечет, ожидая, когда объект станет доступным.
  • Добавить метод Recycle(T item) для обработки ситуаций, когда объект должен быть переработан, например, когда происходит сбой.

Ответ 7

Это еще одна реализация с ограниченным количеством объектов в пуле.

public class ObjectPool<T>
    where T : class
{
    private readonly int maxSize;
    private Func<T> constructor;
    private int currentSize;
    private Queue<T> pool;
    private AutoResetEvent poolReleasedEvent;

    public ObjectPool(int maxSize, Func<T> constructor)
    {
        this.maxSize = maxSize;
        this.constructor = constructor;
        this.currentSize = 0;
        this.pool = new Queue<T>();
        this.poolReleasedEvent = new AutoResetEvent(false);
    }

    public T GetFromPool()
    {
        T item = null;
        do
        {
            lock (this)
            {
                if (this.pool.Count == 0)
                {
                    if (this.currentSize < this.maxSize)
                    {
                        item = this.constructor();
                        this.currentSize++;
                    }
                }
                else
                {
                    item = this.pool.Dequeue();
                }
            }

            if (null == item)
            {
                this.poolReleasedEvent.WaitOne();
            }
        }
        while (null == item);
        return item;
    }

    public void ReturnToPool(T item)
    {
        lock (this)
        {
            this.pool.Enqueue(item);
            this.poolReleasedEvent.Set();
        }
    }
}