Реалізація шаблону об'єднання об'єктів C #


165

Хтось має хороший ресурс для реалізації спільної стратегії пулу об'єктів для обмеженого ресурсу у поєднанні об'єднання Sql? (тобто буде реалізовано повністю, щоб це було безпечно для потоків).

Здійснювати подальші дії стосовно @Aaronaught запиту на роз'яснення використання пулу було б для запитів на балансування навантаження до зовнішньої служби. Поставити це за сценарієм, який, мабуть, було б простіше зрозуміти відразу, на відміну від мого прямого розташування. У мене є об'єкт сеансу, який функціонує аналогічно ISessionоб'єкту від NHibernate. Щоб кожен унікальний сеанс керував його підключенням до бази даних. В даний час у мене є один тривалий об’єкт сеансу, і у мене виникають проблеми, коли мій постачальник послуг обмежує швидкість мого використання цього індивідуального сеансу.

Через їх відсутність сподівання, що один сеанс буде розглядатися як тривалий обліковий запис служби, вони, мабуть, трактують його як клієнта, який забиває їх службу. Що приводить мене до мого запитання тут, замість того, щоб проводити один окремий сеанс, я створив би пул різних сесій і розділив запити до служби на ці кілька сеансів, а не створив єдину координаційну точку, як я це робив раніше.

Сподіваємось, що цей фон пропонує певну цінність, але безпосередньо відповісти на деякі ваші запитання:

З: Чи дорого створювати об’єкти?
Відповідь: Жоден об'єкт не є об'єктом обмежених ресурсів

Питання: Чи будуть їх купувати / звільняти дуже часто?
Відповідь: Так, ще раз їх можна подумати про ІНСІБОРНІ ІСЕСІЇ, де 1 зазвичай здобувається та випускається протягом кожного запиту на одну сторінку.

Питання: Чи достатньо буде простого першого приходу-першої подачі або вам потрібно щось розумніше, тобто це запобігло б голодуванню?
A: Простий розподіл круглих робінів був би достатнім через голодування. Я вважаю, що ви маєте на увазі, якщо немає доступних сеансів, що абонентів заблокують в очікуванні випусків. Це дійсно не застосовується, оскільки сеанси можуть бути спільними для різних абонентів. Моя мета - розподілити використання на кілька сеансів на відміну від 1 одного сеансу.

Я вважаю, що це, мабуть, відмінність від звичайного використання об'єктного пулу, тому я спочатку залишив цю частину поза та планував просто адаптувати схему, щоб дозволити спільний доступ до об'єктів, а не дозволяти коли-небудь виникати ситуації голоду.

Питання: Що з такими речами, як пріоритети, ледачі проти прагнення завантаження тощо?
Відповідь: Ніякої пріоритетності немає, для простоти просто припускаю, що я створив би пул доступних об'єктів при створенні самого пулу.


1
Чи можете ви розповісти нам трохи про свої вимоги? Не всі басейни створюються рівними. Чи дорого створювати об’єкти? Чи будуть їх набувати / звільняти дуже часто? Чи вистачить простого приходу-першої служби або вам потрібно щось розумніше, тобто це запобігло б голодуванню? Що з такими речами, як пріоритети, ледачий та прагнутий завантаження тощо? Все, що ви можете додати, допоможе нам (або принаймні мені) придумати більш ретельну відповідь.
Aaronaught

Кріс - просто дивлячись на твій 2-й та 3-й абзаци і цікавишся, чи справді ці сеанси слід тримати в живих нескінченно? Це здається, що саме це не подобається вашому постачальнику послуг (тривалі сесії), і тому ви можете шукати реалізацію пулу, яка за необхідності розгортає нові сеанси та вимикає їх, коли вони не використовуються (через деякий визначений період) . Це можна зробити, але це трохи складніше, тому я хотів би підтвердити.
Aaronaught

Я не впевнений, чи потрібен мені надійний варіант рішення, чи ні ще, оскільки моє рішення просто гіпотетичне. Можливо, що мій постачальник послуг мене просто бреше, і що їхня послуга надмірно продана, і вони просто знайшли привід для способу звинуватити користувача.
Кріс Марісіч

1
Я думаю, що TPL DataFlow BufferBlock робить більшу частину того, що вам потрібно.
витрачати

1
Об'єднання в потокових середовищах - це повторювана проблема, вирішена за допомогою моделей дизайну, таких як Ресурсний пул і Кеш ресурсів. Ознайомтеся з візерунковою архітектурою програмного забезпечення, том 3: Шаблони управління ресурсами для отримання додаткової інформації.
Фурманатор

Відповіді:


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. Оскільки він знаходиться в ядрі BCL дотнету, ASP.NET Core може поділитися своїм об'єктним пулом з іншими об'єктами, такими як серійний механізм JSON Newtonsoft.Json. Ви можете прочитати цю публікацію в блозі для отримання додаткової інформації про те, як це робить Newtonsoft.Json.

Об'єднання об'єднань у Microsoft Roslyn C # Compiler

Новий компілятор Microsoft Roslyn C # містить тип ObjectPool , який використовується для об'єднання часто використовуваних об'єктів, які зазвичай отримують нові речі, а сміття збирається дуже часто. Це зменшує кількість та розмір операцій з вивезення сміття, які мають відбутися. Існує кілька різних підреалізацій, які використовують ObjectPool (Див.: Чому в Росліні так багато реалізацій об'єднаного об'єднання? ).

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. Вона має точно таку саму семантику. Його розробили інженери Бінга. Прочитайте тут повідомлення в блозі або подивіться код на 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, я, швидше за все, зробить це прийнятим відповіддю, оскільки це, очевидно, найкраще з усіх, якщо він так налаштований, що його використовує сама Росільн.
Кріс Марісіч

Я згоден, але яку реалізацію ви б використали? Рослін містить три. Дивіться посилання на моє запитання у відповіді.
Мухаммад Рехан Саїд

1
Здається, кожен має дуже чітко визначені цілі, набагато краще, ніж лише вибір відкритого кінця, один розмір підходить для всього взуття.
Кріс Марісіч

1
@MuhammadRehanSaeed чудовим доповненням до ArrayPool
Chris Marisic

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 вже має ідеально хороший інструмент для цього, він називається 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для контролю паралельності замість того, щоб релігійно перевіряти стан магазину товарів. Поки придбані предмети будуть правильно випущені, хвилюватися нема про що.

І останнє, але не менш важливе, є прибирання:

    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, але я не вникаю в це). Він також підтримує посилання на те, 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 для вашого задоволення від копіювання та вставки. Існує також коротка програма тестування, яку можна використовувати для розігрування з різними режимами завантаження / доступу та багатопотоковими умовами, щоб переконатись у тому, що це безпечно для потоків і не є помилкою.

Повідомте мене, якщо у вас є якісь запитання чи проблеми.


62
Один із найповніших, корисних та цікавих відповідей, які я читав у програмі SO.
Джош Смітон

Я не міг більше погодитися з @Josh щодо такої відповіді, особливо для частини PooledFoo, оскільки вивільнення об'єктів завжди здавалося, що вони обробляються дуже герметично, і я гадав, що було б найбільш сенсом мати можливість використовувати використання така конструкція, як ви показали, що я просто не сів і намагався побудувати це там, де ваша відповідь дає мені всю інформацію, яка мені могла знадобитися для вирішення моєї проблеми. Я думаю, що для моєї конкретної ситуації я зможу це трохи спростити, головним чином, оскільки я можу ділитися екземплярами між потоками і не потрібно відпускати їх назад до пулу.
Кріс Марісіч

Однак якщо простий підхід спочатку не працює, у мене в голові є кілька ідей, як я міг розумно поводитися з релізом для моєї справи. Думаю, що конкретніше я б встановив випуск, щоб можна було визначити, що сесія сама по собі винила, і розпорядитися нею та замінити нову в пул. Незважаючи на те, що ця публікація зараз є остаточним керівництвом щодо об'єднання об'єктів у C # 3.0, я з нетерпінням чекаю, чи є ще хтось із цим коментарів.
Кріс Марісіч

@Chris: Якщо ви говорите про клієнтських проксі-сервісів WCF, я також маю для цього схему, хоча для ефективного використання вам потрібен інжектор залежності або перехоплювач методу. Версія DI використовує ядро ​​зі спеціальним провайдером для отримання нової версії, якщо виникла помилка, версія перехоплення методу (на мою перевагу) просто загортає існуючий проксі і вставляє перевірку помилок перед кожною. Я не впевнений, наскільки легко було б інтегрувати його в такий пул (не дуже намагався, оскільки я це лише написав!), Але це, безумовно, було б можливо.
Aaronaught

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);
        }
    }
}

1
Закресліть цей коментар раніше. Я думаю, що я просто здався дивним, тому що цей пул, здається, не має порогових значень, а може, і не потрібно, це залежатиме від вимог.
Aaronaught

1
@Aaronaught - Це справді це дивно? Мені хотілося створити легкий басейн, який пропонує лише необхідну функціональність. Клієнт повинен правильно використовувати клас.
ChaosPandion

1
+1 для дуже простого рішення, яке можна адаптувати до моїх цілей, просто змінивши тип підкладки на List / HashTable тощо та змінивши лічильник на перевертання. Випадкове запитання, як ви обробляєте управління об'єктом пулу? Ви просто вставляєте його в контейнер МОК, визначаючи, що він там однотонний?
Кріс Марісіч

1
Це має бути статичним лише для читання? Але мені здається дивним, що ви б поставили всередину остаточне висловлювання, якщо є виняток, чи не було б ймовірним, що об'єкт, який він сам, винен? Чи Putвдалося б ви обробити це всередині методу і залишити його для простоти певного типу перевірки того, чи об'єкт винен, і створити новий екземпляр, який слід додати до пулу замість того, щоб вставляти попередній?
Кріс Марісіч

1
@Chris - Я просто пропоную простий інструмент, який я вважав корисним у минулому. Решта - за вами. Змініть та використовуйте код так, як вважаєте за потрібне.
ChaosPandion

6

Дякуємо за це посилання Обмеження розміру для цієї реалізації не існує, тому якщо у вас є шип у створенні об'єктів, ці екземпляри ніколи не збираються і, ймовірно, ніколи не використовуються, поки не з’явиться інший шип. Це дуже просто і легко зрозуміти, і не важко буде додати максимальний обмеження розміру.
Мухаммед Рехан Саїд

Приємно і просто
Даніель де Зван

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

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

  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();
        }
    }
}



Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.