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

165

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

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

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

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

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

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

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

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

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

Крис Марисик
источник
1
Можете ли вы рассказать нам немного о ваших требованиях? Не все пулы созданы равными. Дорогие объекты для создания? Будут ли они приобретаться / выпускаться очень часто? Будет ли достаточно простого «первым пришел - первым обслужен», или вам нужно что-то более умное, то есть, чтобы предотвратить голод? А как насчет таких вещей, как приоритеты, ленивая или энергичная загрузка и т. Д.? Все, что вы можете добавить, поможет нам (или, по крайней мере, мне) придумать более подробный ответ.
Aaronaught
Крис - просто смотрите на ваши 2-й и 3-й абзацы и задаетесь вопросом, должны ли эти сессии действительно оставаться в живых до бесконечности? Похоже, что это не нравится вашему поставщику услуг (длительные сеансы), и поэтому вы можете искать реализацию пула, которая при необходимости запускает новые сеансы и закрывает их, когда они не используются (после определенного периода времени) , Это можно сделать, но это немного сложнее, поэтому я хотел бы подтвердить.
Aaronaught
Я не уверен, нужно ли мне это надежное решение или нет, так как мое решение просто гипотетическое. Вполне возможно, что мой поставщик услуг просто лжет мне и что его услуга перепродана, и он просто нашел оправдание тому, чтобы обвинить пользователя.
Крис Марисик
1
Я думаю, что TPL DataFlow BufferBlock делает большую часть того, что вам нужно.
спонсор
1
Пул в многопоточных средах - это повторяющаяся проблема, решаемая такими шаблонами проектирования, как Resource Pool и Resource Cache. Проверьте Pattern-Oriented Software Architecture, том 3: Шаблоны для управления ресурсами для получения дополнительной информации.
Fuhrmanator

Ответы:

59

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

Ядро DotNet имеет реализацию пула объектов добавленного в библиотеку базовых классов (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. Поскольку ASP.NET Core находится в ядре dotnet BCL, он может совместно использовать свой пул объектов с другими объектами, такими как сериализатор JSON Newtonsoft.Json. Вы можете прочитать этот блог для получения дополнительной информации о том, как Newtonsoft.Json делает это.

Пул объектов в компиляторе Microsoft Roslyn C #

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

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 , приведенные выше , в частности , для списка и 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
Это отличный ответ. После того, как C # 6 и VS2015 будут RTM, я, вероятно, сделаю это приемлемым ответом, так как он явно лучше, если он так настроен, что используется самой Rosyln.
Крис Марисик
Я согласен, но какую реализацию вы бы использовали? Рослин содержит три. Смотрите ссылку на мой вопрос в ответе.
Мухаммед Рехан Саид
1
Похоже , у каждого есть очень четко определенные цели, гораздо лучше , чем только выбор из открытых завершившийся один размер подходит всем ботинком.
Крис Марисик
1
@MuhammadRehanSaeed отличное дополнение с ArrayPool
Крис Марисич
1
Видя , RecyclableMemoryStreamчто это удивительное дополнение для ультра оптимизации высокой производительности.
Крис Марисик
315

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

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

У пула общего назначения должно быть несколько основных «настроек», включая:

  • Стратегия загрузки ресурсов - нетерпеливый или ленивый;
  • Механизм загрузки ресурсов - как его реально создать;
  • Стратегия доступа - вы упоминаете «круговой прием», который не так прост, как кажется; эта реализация может использовать циклический буфер, который похож , но не идеален, потому что пул не контролирует, когда ресурсы фактически возвращаются. Другие варианты: 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 предметов, и в типичном случае, когда ресурсы используются слабо, нужно продвинуть только один или два слота.

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

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

// 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 уже есть достаточно хороший инструмент для этого, он называется Семафор и разработан специально для разрешения фиксированных количество потоков доступа к ресурсу (в данном случае «ресурс» является внутренним хранилищем элементов). Поскольку мы не внедряем полноценную очередь производителей / потребителей, это вполне соответствует нашим потребностям.

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

    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контроль параллелизма вместо религиозной проверки состояния хранилища элементов. Пока приобретенные предметы правильно выпущены, не о чем беспокоиться.

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

    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>качестве провайдера / фабрики.


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

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

Aaronaught
источник
62
Один из самых полных, полезных и интересных ответов, которые я читал на SO.
Джош Смитон
Я не мог согласиться с @Josh об этом ответе, особенно для части PooledFoo, так как выпуск объектов всегда казался очень запутанным, и я подумал, что было бы разумнее иметь возможность использовать построить, как вы показали, я просто не сел и попытался построить это, где, как ваш ответ дает мне всю информацию, которая мне может понадобиться для решения моей проблемы. Я думаю, что для моей конкретной ситуации я смогу немного упростить это, главным образом, так как я могу делить экземпляры между потоками и не нужно выпускать их обратно в пул.
Крис Марисик
Однако, если простой подход не работает в первую очередь, у меня в голове есть несколько идей о том, как я могу разумно справиться с выпуском для моего случая. Я думаю, что наиболее конкретно я бы установил релиз, чтобы иметь возможность определить, что сам сеанс вышел из строя, избавиться от него и заменить новый в пул. Несмотря на то, что этот пост на данный момент является в значительной степени исчерпывающим руководством по объединению объектов в C # 3.0, я с нетерпением жду возможности увидеть, есть ли у кого-нибудь еще комментарии по этому поводу.
Крис Марисик
@Chris: Если вы говорите о прокси-клиентах WCF, у меня также есть образец для этого, хотя вам нужен инжектор зависимостей или метод-перехватчик для его эффективного использования. Версия DI использует ядро ​​с пользовательским провайдером для получения новой версии, если произошла ошибка, версия перехвата метода (мои предпочтения) просто оборачивает существующий прокси и вставляет проверку ошибки перед каждым. Я не уверен, насколько легко было бы интегрировать его в пул, подобный этому (на самом деле не пытался, так как я только что написал это!), Но это определенно было бы возможно.
Аарона
5
Очень впечатляет, хотя и немного перегружен для большинства ситуаций. Я ожидаю, что что-то подобное будет частью структуры.
ChaosPandion
7

Нечто подобное может удовлетворить ваши потребности.

/// <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);
        }
    }
}
ChaosPandion
источник
1
Поцарапайте этот предыдущий комментарий. Я думаю, что мне показалось странным, потому что у этого пула нет порогов, и, возможно, в этом нет необходимости, это будет зависеть от требований.
Aaronaught
1
@Aaronaught - Это действительно так странно? Я хотел создать легкий пул, который предлагает только необходимую функциональность. Клиент должен правильно использовать класс.
ChaosPandion
1
+1 за очень простое решение, которое можно адаптировать к моим целям, просто изменив тип подложки на List / HashTable и т. Д. И изменив счетчик для пролонгации. Случайный вопрос, как вы справляетесь с управлением объектом пула? Вы просто помещаете это в контейнер IOC, определяя это, чтобы быть единственным?
Крис Марисик
1
Это должно быть статическим только для чтения? Но я нахожу странным, что вы поместили бы внутри оператора finally, если есть исключение, не будет ли вероятность того, что объект сам по себе является ошибочным? Не могли бы вы обработать это внутри Putметода и оставить для простоты какой-то тип проверки того, является ли объект неисправным, и создать новый экземпляр для добавления в пул вместо вставки предыдущего?
Крис Марисик
1
@Chris - я просто предлагаю простой инструмент, который я нашел полезным в прошлом. Остальное зависит от тебя. Измените и используйте код по своему усмотрению.
ChaosPandion
6

Пример из MSDN: Как создать пул объектов с помощью ConcurrentBag

Томас Муцль
источник
Спасибо за эту ссылку. Для этой реализации нет ограничения по размеру, поэтому, если у вас есть всплеск создания объекта, эти экземпляры никогда не будут собраны и, вероятно, никогда не будут использованы, пока не будет другой всплеск. Это очень просто и легко понять, и не будет трудно добавить ограничение максимального размера.
Мухаммед Рехан Саид
Даниэль де Цваан
4

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

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

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

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

Томас
источник
+1 за то, что показал мне, что IInstanceProviderинтерфейс существует, так как я буду реализовывать это для своего решения. Я всегда любил размещать свой код за предоставленным Microsoft интерфейсом, когда они дают подходящее определение.
Крис Марисик
4

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

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

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

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();
        }
    }
}
Питер К.
источник