Реализация Шаблона Пулинга объектов C#

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

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

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

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

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

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

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

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

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

160
задан Chris Marisic 2 April 2010 в 18:52
поделиться

5 ответов

Ориентированная на Java, эта статья раскрывает шаблон пула connectionImpl и шаблон пула абстрактных объектов и может быть хорошим первым подходом: http://www.developer.com/design/article.php /626171/Pattern-Summaries-Object-Pool.htm

Шаблон пула объектов:

pattern

3
ответ дан 23 November 2019 в 21:29
поделиться

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

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

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

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

Все перечисленные ниже классы являются внутренними классами пула 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 уже есть отличный инструмент для этого, он называется 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();
    }

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

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

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


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

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

309
ответ дан 23 November 2019 в 21:29
поделиться

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

/// <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);
        }
    }
}
7
ответ дан 23 November 2019 в 21:29
поделиться

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

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

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

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

4
ответ дан 23 November 2019 в 21:29
поделиться

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

  1. Измените sync.WaitOne () на sync.WaitOne (timeout) и выставьте тайм-аут в качестве параметра в Acquire (int timeout) метод. Это также потребует обработки условия, когда поток истекает, ожидая, когда объект станет доступным.
  2. Добавьте метод Recycle (T item) для обработки ситуаций, когда объект необходимо перезапустить, например, при возникновении сбоя.
4
ответ дан 23 November 2019 в 21:29
поделиться
Другие вопросы по тегам:

Похожие вопросы: