Implementacja wzorca puli obiektów w języku C #

165

Czy ktoś ma dobry zasób dotyczący wdrażania strategii puli obiektów współużytkowanych dla ograniczonego zasobu w duchu puli połączeń Sql? (tj. zostałby zaimplementowany w pełni, że jest bezpieczny dla wątków).

Aby odpowiedzieć na prośbę @Aaronaught o wyjaśnienie, użycie puli będzie dotyczyło żądań równoważenia obciążenia do usługi zewnętrznej. Ujmując to w scenariuszu, który prawdopodobnie byłby łatwiejszy do natychmiastowego zrozumienia, w przeciwieństwie do mojej bezpośredniej sytuacji. Mam obiekt sesji, który działa podobnie do ISessionobiektu z NHibernate. Że każda unikalna sesja zarządza połączeniem z bazą danych. Obecnie mam 1 długo działający obiekt sesji i napotykam problemy, w których mój dostawca usług ogranicza szybkość korzystania z tej indywidualnej sesji.

Ponieważ nie spodziewają się, że pojedyncza sesja będzie traktowana jako długotrwałe konto usługi, najwyraźniej traktują ją jako klienta, który hamuje ich usługę. Co prowadzi mnie tutaj do mojego pytania, zamiast mieć jedną indywidualną sesję, utworzyłbym pulę różnych sesji i podzielił żądania na usługę w tych wielu sesjach, zamiast tworzyć pojedynczy punkt centralny, jak robiłem wcześniej.

Miejmy nadzieję, że to tło ma jakąś wartość, ale aby bezpośrednio odpowiedzieć na niektóre pytania:

P: Czy tworzenie obiektów jest drogie?
O: Żadne obiekty nie są pulą ograniczonych zasobów

P: Czy będą często nabywane / wydawane?
Odp .: Tak, po raz kolejny można je pomyśleć o NHibernate ISessions, w których 1 jest zwykle nabywane i wydawane na czas trwania każdego żądania strony.

P: Czy wystarczy zwykła zasada „kto pierwszy, ten lepszy”, czy potrzebujesz czegoś bardziej inteligentnego, tj. Co zapobiegnie głodowi?
O: Prosta dystrybucja typu round robin byłaby wystarczająca, zakładam, że przez głód masz na myśli, że jeśli nie ma dostępnych sesji, dzwoniący zostaną zablokowani, czekając na wydania. To nie ma zastosowania, ponieważ sesje mogą być współużytkowane przez różnych rozmówców. Moim celem jest rozłożenie wykorzystania na wiele sesji, a nie na jedną sesję.

Uważam, że jest to prawdopodobnie odejście od normalnego korzystania z puli obiektów, dlatego pierwotnie pominąłem tę część i planowałem tylko dostosować wzór, aby umożliwić dzielenie się obiektami, a nie pozwolić, aby kiedykolwiek wystąpiła sytuacja głodowa.

P: A co z takimi rzeczami, jak priorytety, leniwe i chętne ładowanie itp.?
Odp .: Nie ma tu żadnego ustalania priorytetów, dla uproszczenia po prostu załóżmy, że utworzę pulę dostępnych obiektów podczas tworzenia samej puli.

Chris Marisic
źródło
1
Czy możesz nam trochę opowiedzieć o swoich wymaganiach? Nie wszystkie pule są sobie równe. Czy obiekty są drogie w tworzeniu? Czy będą często zdobywane / zwalniane? Czy wystarczy zwykła zasada „kto pierwszy, ten lepszy”, czy też potrzebujesz czegoś bardziej inteligentnego, tj. Co zapobiegnie głodowi? A co z takimi rzeczami, jak priorytety, lenistwo vs. Wszystko, co możesz dodać, pomoże nam (a przynajmniej mi) w znalezieniu dokładniejszej odpowiedzi.
Aaronaught
Chris - po prostu patrząc na twój drugi i trzeci akapit i zastanawiasz się, czy te sesje naprawdę powinny trwać w nieskończoność? Wygląda na to, że usługodawca nie lubi tego (sesje długotrwałe), więc możesz szukać implementacji puli, która w razie potrzeby podkręca nowe sesje i wyłącza je, gdy nie są używane (po pewnym określonym czasie) . Można to zrobić, ale jest to trochę bardziej skomplikowane, więc chciałbym to potwierdzić.
Aaronaught
Nie jestem pewien, czy potrzebuję tak solidnego rozwiązania, czy jeszcze nie, ponieważ moje rozwiązanie jest tylko hipotetyczne. Możliwe, że mój usługodawca po prostu mnie okłamuje, a jego usługi są zbyt popularne i po prostu znaleźli wymówkę, aby obwinić użytkownika.
Chris Marisic
1
Myślę, że TPL DataFlow BufferBlock robi większość tego, czego potrzebujesz.
wydający
1
Pule w środowiskach wielowątkowych to powtarzający się problem, który rozwiązują wzorce projektowe, takie jak pula zasobów i pamięć podręczna zasobów. Zobacz temat Architektura oprogramowania zorientowana na wzorce, tom 3: Wzorce zarządzania zasobami, aby uzyskać więcej informacji.
Fuhrmanator

Odpowiedzi:

59

Pule obiektów w .NET Core

Rdzeń DotNet ma realizację łączenia przedmiotów dodaje się do biblioteki klas bazowej (BCL). Możesz przeczytać oryginalny numer GitHub tutaj i wyświetlić kod dla System.Buffers . Obecnie ArrayPooljest jedynym dostępnym typem i służy do łączenia tablic. Jest ładny blogu tutaj .

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

Przykład jego użycia można zobaczyć w ASP.NET Core. Ponieważ znajduje się w rdzeniu dotnet BCL, ASP.NET Core może udostępniać swoją pulę obiektów innym obiektom, takim jak serializator JSON Newtonsoft.Json. Możesz przeczytać ten wpis na blogu, aby uzyskać więcej informacji o tym, jak robi to Newtonsoft.Json.

Pule obiektów w kompilatorze Microsoft Roslyn C #

Nowy kompilator Microsoft Roslyn C # zawiera typ ObjectPool , który jest używany do gromadzenia często używanych obiektów, które normalnie byłyby bardzo często aktualizowane i zbierane jako śmieci. Zmniejsza to ilość i rozmiar operacji czyszczenia pamięci, które muszą się wydarzyć. Istnieje kilka różnych implementacji podrzędnych, z których wszystkie używają ObjectPool (patrz: Dlaczego w Roslyn jest tak wiele implementacji puli obiektów? ).

1 - SharedPools - Przechowuje pulę 20 obiektów lub 100, jeśli używana jest 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 i StringBuilderPool - Nie są to ściśle oddzielne implementacje, ale otoki wokół implementacji SharedPools pokazanej powyżej, szczególnie dla List i StringBuilder's. Więc to ponownie wykorzystuje pulę obiektów przechowywanych w 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 i PooledHashSet - używają bezpośrednio ObjectPool i mają całkowicie oddzielną pulę obiektów. Przechowuje pulę 128 obiektów.

// 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

Ta biblioteka zapewnia pule dla MemoryStreamobiektów. To zastępczy zamiennik System.IO.MemoryStream. Ma dokładnie tę samą semantykę. Został zaprojektowany przez inżynierów Bing. Przeczytaj wpis na blogu tutaj lub zobacz kod na 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); 
}

Zauważ, że RecyclableMemoryStreamManagerpowinno zostać zadeklarowane raz, a będzie działać przez cały proces - to jest pula. Jeśli chcesz, możesz używać wielu basenów.

Muhammad Rehan Saeed
źródło
2
To świetna odpowiedź. Po tym, jak C # 6 i VS2015 to RTM, prawdopodobnie uznam to za akceptowaną odpowiedź, ponieważ jest to zdecydowanie najlepsze ze wszystkich, jeśli jest tak dostrojone, że jest używane przez sam Rosyln.
Chris Marisic
Zgadzam się, ale z której implementacji skorzystasz? Roslyn zawiera trzy. Zobacz link do mojego pytania w odpowiedzi.
Muhammad Rehan Saeed
1
Wygląda na to, każdy z nich ma bardzo jasno określone cele, dużo lepiej niż tylko wybór otwartego zakończony jednym rozmiarze buta.
Chris Marisic
1
@MuhammadRehanSaeed świetny dodatek z ArrayPool
Chris
1
Widząc to RecyclableMemoryStream, jest to niesamowity dodatek do ultra wysokiej optymalizacji wydajności.
Chris Marisic,
315

To pytanie jest trochę trudniejsze, niż można by się spodziewać, z powodu kilku niewiadomych: zachowanie zasobu, który jest gromadzony, oczekiwany / wymagany czas życia obiektów, rzeczywisty powód, dla którego pula jest wymagana itp. Zazwyczaj pule są specjalnego przeznaczenia - wątek pule, pule połączeń itp. - ponieważ łatwiej jest je zoptymalizować, gdy wiesz dokładnie, co robi zasób, a co ważniejsze, masz kontrolę nad tym, jak ten zasób jest wdrażany.

Ponieważ nie jest to takie proste, starałem się zaproponować dość elastyczne podejście, z którym można eksperymentować i zobaczyć, co działa najlepiej. Z góry przepraszamy za długi post, ale jest wiele powodów do omówienia, jeśli chodzi o wdrożenie przyzwoitej puli zasobów ogólnego przeznaczenia. i tak naprawdę tylko drapię powierzchnię.

Pula ogólnego przeznaczenia musiałaby mieć kilka głównych „ustawień”, w tym:

  • Strategia ładowania zasobów - chętna lub leniwa;
  • Mechanizm ładowania zasobów - jak właściwie go zbudować;
  • Strategia dostępu - wspominasz o „okrągłym robocie”, który nie jest tak prosty, jak się wydaje; ta implementacja może korzystać z bufora cyklicznego, który jest podobny , ale nie doskonały, ponieważ pula nie ma kontroli nad faktycznym odzyskiwaniem zasobów. Inne opcje to FIFO i LIFO; FIFO będzie mieć więcej wzorca dostępu losowego, ale LIFO znacznie ułatwia wdrożenie strategii zwalniania najmniej ostatnio używanych (która, jak powiedziałeś, wykracza poza zakres, ale nadal warto o tym wspomnieć).

W przypadku mechanizmu ładowania zasobów .NET już daje nam czystą abstrakcję - delegatów.

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

Prześlij to przez konstruktor puli i już z tym skończymy. Używanie typu ogólnego z new()ograniczeniem również działa, ale jest to bardziej elastyczne.


Z pozostałych dwóch parametrów strategia dostępu jest bardziej skomplikowaną bestią, więc moje podejście polegało na zastosowaniu podejścia opartego na dziedziczeniu (interfejsie):

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

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

Koncepcja tutaj jest prosta - pozwolimy Poolklasie publicznej zająć się typowymi problemami, takimi jak bezpieczeństwo wątków, ale używamy innego „magazynu elementów” dla każdego wzorca dostępu. LIFO jest łatwo reprezentowane przez stos, FIFO to kolejka, a ja użyłem niezbyt zoptymalizowanej, ale prawdopodobnie wystarczającej implementacji bufora cyklicznego, używającej List<T>wskaźnika i indeksu do przybliżenia wzorca dostępu typu round-robin.

Wszystkie poniższe klasy są wewnętrznymi klasami Pool<T>- to był wybór stylu, ale ponieważ tak naprawdę nie są przeznaczone do użytku poza nimi Pool, ma to największy sens.

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

To są oczywiste - stos i kolejka. Nie sądzę, żeby naprawdę uzasadniały wiele wyjaśnień. Bufor kołowy jest trochę bardziej skomplikowany:

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

Mogłem wybrać wiele różnych podejść, ale najważniejsze jest to, że zasoby powinny być dostępne w tej samej kolejności, w jakiej zostały utworzone, co oznacza, że ​​musimy zachować odniesienia do nich, ale oznaczyć je jako „w użyciu” (lub nie ). W najgorszym przypadku dostępne jest tylko jedno gniazdo, a każde pobranie wymaga pełnej iteracji bufora. Jest to złe, jeśli masz zebrane setki zasobów i pozyskujesz je i zwalniasz kilka razy na sekundę; nie stanowi to problemu w przypadku puli 5-10 przedmiotów, aw typowym przypadku, gdy zasoby są słabo wykorzystywane, wystarczy przesunąć o jeden lub dwa miejsca.

Pamiętaj, te klasy są prywatnymi klasami wewnętrznymi - dlatego nie potrzebują dużo sprawdzania błędów, sama pula ogranicza do nich dostęp.

Wrzuć wyliczenie i metodę fabryczną i skończymy z tą częścią:

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

Następnym problemem do rozwiązania jest strategia ładowania. Zdefiniowałem trzy typy:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Pierwsze dwa powinny być oczywiste; trzeci jest rodzajem hybrydy, leniwie ładuje zasoby, ale w rzeczywistości nie zaczyna ich ponownie używać, dopóki pula nie zostanie zapełniona. Byłby to dobry kompromis, jeśli chcesz, aby pula była pełna (co brzmi tak, jakbyś to robiła), ale chcesz odłożyć koszt faktycznego utworzenia ich do pierwszego dostępu (tj. Aby poprawić czas uruchamiania).

Metody ładowania naprawdę nie są zbyt skomplikowane, teraz, gdy mamy abstrakcję sklepu z przedmiotami:

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

Powyższe pola sizei countodnoszą się odpowiednio do maksymalnego rozmiaru puli i całkowitej liczby zasobów należących do puli (ale niekoniecznie dostępnych ). AcquireEagerjest najprostszy, zakłada, że ​​towar jest już w sklepie - elementy te zostałyby załadowane na etapie budowy, czyli w PreloadItemssposób pokazany jako ostatni.

AcquireLazysprawdza, czy w puli są wolne elementy, a jeśli nie, tworzy nowy. AcquireLazyExpandingutworzy nowy zasób, o ile pula nie osiągnęła jeszcze swojego docelowego rozmiaru. Próbowałem w celu optymalizacji tego celu zminimalizowania blokowania i mam nadzieję, że nie popełniłem żadnych błędów (I zostały przetestowane w warunkach wielowątkowych, ale oczywiście nie w sposób wyczerpujący).

Możesz się zastanawiać, dlaczego żadna z tych metod nie zawraca sobie głowy sprawdzaniem, czy sklep osiągnął maksymalny rozmiar. Zaraz do tego dojdę.


Teraz czas na sam basen. Oto pełny zestaw prywatnych danych, z których część została już pokazana:

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

Odpowiadając na pytanie, które przemilczałem w ostatnim akapicie - jak zapewnić ograniczenie całkowitej liczby tworzonych zasobów - okazuje się, że .NET ma już do tego doskonale dobre narzędzie, nazywa się Semafor i jest zaprojektowane specjalnie, aby umożliwić naprawę liczba wątków dostępu do zasobu (w tym przypadku „zasób” to wewnętrzny magazyn elementów). Ponieważ nie wdrażamy pełnej kolejki producent / konsument, jest to całkowicie adekwatne do naszych potrzeb.

Konstruktor wygląda tak:

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

Nie powinno tu być żadnych niespodzianek. Jedyną rzeczą, na którą należy zwrócić uwagę, jest specjalna obudowa do szybkiego ładowania, przy użyciu PreloadItemsmetody już przedstawionej wcześniej.

Ponieważ prawie wszystko zostało już czysto wyabstrahowane, rzeczywistość Acquirei Releasemetody są naprawdę bardzo proste:

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

Jak wyjaśniono wcześniej, używamy Semaphoredo kontrolowania współbieżności zamiast religijnego sprawdzania statusu sklepu z przedmiotami. Dopóki zdobyte przedmioty są prawidłowo wydawane, nie ma się czym martwić.

Wreszcie, jest porządek:

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

Cel tej IsDisposedwłasności stanie się jasny za chwilę. Jedyną główną Disposemetodą jest usunięcie rzeczywistych pozycji w puli, jeśli są one implementowane IDisposable.


Teraz możesz zasadniczo używać tego tak, jak jest, z try-finallyblokiem, ale nie przepadam za tą składnią, ponieważ jeśli zaczniesz przekazywać zasoby w puli między klasami i metodami, stanie się to bardzo zagmatwane. Jest możliwe, że główne klasy, który korzysta z zasobów nawet nie mają odniesienie do basenu. Naprawdę robi się dość bałagan, więc lepszym podejściem jest utworzenie „inteligentnego” obiektu w puli.

Powiedzmy, że zaczynamy od następującego prostego interfejsu / klasy:

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

Oto nasz udawany Foozasób jednorazowego użytku, który implementuje IFooi ma pewien standardowy kod do generowania unikalnych tożsamości. Tworzymy kolejny specjalny obiekt w puli:

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

To po prostu przekazuje wszystkie „prawdziwe” metody do swojego wewnętrznego IFoo(moglibyśmy to zrobić z biblioteką Dynamic Proxy, taką jak Castle, ale nie będę się w to zagłębiał). Utrzymuje również odniesienie do tego, Poolktóry go tworzy, więc kiedy my Disposeten obiekt, automatycznie zwalnia się z powrotem do puli. Z wyjątkiem sytuacji, gdy pula została już usunięta - oznacza to, że jesteśmy w trybie „czyszczenia” iw tym przypadku faktycznie czyści zasoby wewnętrzne .


Korzystając z powyższego podejścia, możemy napisać kod w następujący sposób:

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

To bardzo dobra rzecz, aby móc to zrobić. Oznacza to, że kod, który wykorzystujeIFoo (w przeciwieństwie do kodu, który ją tworzy) faktycznie nie trzeba zdawać sobie sprawę z basenu. Możesz nawet wstrzykiwać IFoo obiekty przy użyciu swojej ulubionej biblioteki DI i Pool<T>dostawcy / fabryki.


Umieściłem cały kod w PasteBin dla przyjemności kopiowania i wklejania. Istnieje również krótki program testowy, którego możesz użyć do zabawy z różnymi trybami ładowania / dostępu i warunkami wielowątkowymi, aby upewnić się, że jest bezpieczny dla wątków i nie zawiera błędów.

Daj mi znać, jeśli masz jakieś pytania lub wątpliwości w związku z którymkolwiek z tych tematów.

Aaronaught
źródło
62
Jedna z najbardziej kompletnych, pomocnych i interesujących odpowiedzi, jakie przeczytałem w SO.
Josh Smeaton
Nie mogłem się bardziej zgodzić z @Josh na temat tej odpowiedzi, szczególnie w przypadku części PooledFoo, ponieważ zwalnianie obiektów zawsze wydawało się być obsługiwane w bardzo nieszczelny sposób i wyobrażałem sobie, że najbardziej sensowne byłoby mieć możliwość użycia konstrukt taki, jak pokazałeś, po prostu nie usiadłem i nie próbowałem zbudować tego, w którym twoja odpowiedź daje mi wszystkie informacje, których potrzebowałem do rozwiązania mojego problemu. Myślę, że w mojej konkretnej sytuacji będę mógł to nieco uprościć, głównie, ponieważ mogę udostępniać wystąpienia między wątkami i nie muszę ich zwalniać z powrotem do puli.
Chris Marisic
Jeśli jednak proste podejście nie zadziała w pierwszej kolejności, mam w głowie kilka pomysłów, w jaki sposób mogę inteligentnie zająć się zwolnieniem w moim przypadku. Myślę, że w szczególności ustaliłbym wydanie, aby móc określić, że sesja jest wadliwa, i pozbyć się jej i zastąpić nową w puli. Bez względu na to, że ten post w tym miejscu jest prawie ostatecznym przewodnikiem po buforowaniu obiektów w C # 3.0, nie mogę się doczekać, aby zobaczyć, czy ktoś ma więcej komentarzy na ten temat.
Chris Marisic
@Chris: Jeśli mówisz o serwerach proxy klienta WCF, mam również do tego wzorzec, chociaż potrzebujesz wtryskiwacza zależności lub przechwytywacza metody, aby go skutecznie używać. Wersja DI używa jądra z niestandardowym dostawcą w celu uzyskania nowej wersji, w przypadku której wystąpił błąd, wersja przechwytywania metody (moje preferencje) po prostu opakowuje istniejący serwer proxy i wstawia sprawdzanie błędów przed każdym. Nie jestem pewien, jak łatwo byłoby zintegrować go z taką pulą (tak naprawdę nie próbowałem, ponieważ właśnie to napisałem!), Ale na pewno byłoby to możliwe.
Aaronaught
5
Bardzo imponujące, choć w większości sytuacji nieco przesadzone. Spodziewałbym się, że coś takiego będzie częścią struktury.
ChaosPandion
7

Coś takiego może odpowiadać Twoim potrzebom.

/// <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)
                {

                }
            }
        }
    }
}

Przykładowe użycie

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
źródło
1
Zdrap ten wcześniejszy komentarz. Myślę, że po prostu wydało mi się to dziwne, ponieważ ta pula nie wydaje się mieć żadnych progów i może nie musi, to zależałoby od wymagań.
Aaronaught
1
@Aaronaught - Czy to naprawdę takie dziwne? Chciałem stworzyć lekki basen, który oferuje tylko potrzebne funkcje. Właściwe użycie klasy należy do klienta.
ChaosPandion
1
+1 za bardzo proste rozwiązanie, które można dostosować do moich celów, po prostu zmieniając typ podkładu na List / HashTable itp. I zmieniając licznik, aby przewinąć. Losowe pytanie, jak sobie radzisz z zarządzaniem samym obiektem puli? Czy po prostu umieszczasz go w kontenerze IOC, definiując go jako pojedynczy?
Chris Marisic
1
Czy powinno to być statyczne tylko do odczytu? Ale wydaje mi się dziwne, że umieściłbyś to w oświadczeniu końcowym, czy jeśli istnieje wyjątek, czy nie byłoby prawdopodobne, że obiekt, do którego sam jest, jest winny? Czy poradziłbyś sobie z tym wewnątrz Putmetody i dla uproszczenia pominąłbyś pewien rodzaj sprawdzania, czy obiekt jest uszkodzony i utworzyć nową instancję, która zostanie dodana do puli zamiast wstawiania poprzedniej?
Chris Marisic
1
@Chris - po prostu oferuję proste narzędzie, które okazało się przydatne w przeszłości. Reszta należy do Ciebie. Zmodyfikuj i użyj kodu według własnego uznania.
ChaosPandion
6

Przykład z MSDN: instrukcje: tworzenie puli obiektów przy użyciu ConcurrentBag

Thomas Mutzl
źródło
Dzięki za ten link. Nie ma limitu rozmiaru dla tej implementacji, więc jeśli masz gwałtowny wzrost w tworzeniu obiektów, te instancje nigdy nie zostaną zebrane i prawdopodobnie nigdy nie zostaną użyte, dopóki nie nastąpi kolejny skok. Jest to jednak bardzo proste i łatwe do zrozumienia, a dodanie maksymalnego limitu rozmiaru nie byłoby trudne.
Muhammad Rehan Saeed
Ładnie i prosto
Daniel de Zwaan,
4

Kiedyś firma Microsoft udostępniła strukturę za pośrednictwem Microsoft Transaction Server (MTS), a później modelu COM +, aby wykonywać pule obiektów COM. Ta funkcjonalność została przeniesiona do System.EnterpriseServices w .NET Framework, a teraz w Windows Communication Foundation.

Pula obiektów w WCF

Ten artykuł pochodzi z platformy .NET 1.1, ale nadal powinien mieć zastosowanie w bieżących wersjach platformy Framework (mimo że preferowaną metodą jest WCF).

Pule obiektów .NET

Tomasz
źródło
+1 za pokazanie mi, że IInstanceProviderinterfejs istnieje, ponieważ zaimplementuję go w moim rozwiązaniu. Zawsze jestem fanem układania mojego kodu za interfejsem dostarczonym przez Microsoft, gdy zapewniają one pasującą definicję.
Chris Marisic
4

Bardzo podoba mi się implementacja Aronaught - zwłaszcza, że ​​obsługuje on czekanie na udostępnienie zasobu za pomocą semafora. Jest kilka dodatków, które chciałbym wprowadzić:

  1. Zmiana sync.WaitOne() do sync.WaitOne(timeout)i narazić timeout jako parametr w Acquire(int timeout)metodzie. Wymagałoby to również obsługi warunku, w którym wątek przekroczy limit czasu oczekiwania na udostępnienie obiektu.
  2. Add Recycle(T item), aby obsłużyć sytuacje, w których obiekt musi zostać odtworzony, na przykład, gdy wystąpi awaria.
Igor Pashchuk
źródło
3

To kolejna implementacja z ograniczoną liczbą obiektów w puli.

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();
        }
    }
}
Peter K.
źródło