Implementierung des C # Object Pooling Pattern


165

Hat jemand eine gute Ressource für die Implementierung einer gemeinsamen Objektpoolstrategie für eine begrenzte Ressource im Sinne des SQL-Verbindungspoolings? (dh würde vollständig implementiert, dass es threadsicher ist).

Um die @ Klaronaught-Anfrage zur Klärung zu verfolgen, würde die Poolnutzung für Lastausgleichsanforderungen an einen externen Dienst verwendet. Um es in ein Szenario zu bringen, das im Gegensatz zu meiner direkten Situation wahrscheinlich leichter sofort zu verstehen wäre. Ich habe ein Sitzungsobjekt, das ähnlich wie das ISessionObjekt von NHibernate funktioniert. Dass jede einzelne Sitzung ihre Verbindung zur Datenbank verwaltet. Derzeit habe ich 1 Sitzungsobjekt mit langer Laufzeit und es treten Probleme auf, bei denen mein Dienstanbieter die Nutzung dieser einzelnen Sitzung durch eine Rate einschränkt.

Aufgrund ihrer mangelnden Erwartung, dass eine einzelne Sitzung als ein langjähriges Dienstkonto behandelt wird, behandeln sie es anscheinend als einen Kunden, der ihren Dienst hämmert. Das bringt mich zu meiner Frage hier: Anstatt eine einzelne Sitzung zu haben, würde ich einen Pool verschiedener Sitzungen erstellen und die Anforderungen auf den Dienst auf mehrere Sitzungen aufteilen, anstatt wie zuvor einen einzelnen Schwerpunkt zu erstellen.

Hoffentlich bietet dieser Hintergrund einen gewissen Wert, aber um einige Ihrer Fragen direkt zu beantworten:

F: Sind die Objekte teuer in der Erstellung?
A: Keine Objekte sind ein Pool begrenzter Ressourcen

F: Werden sie sehr häufig erworben / freigegeben?
A: Ja, wieder einmal können sie an NHibernate ISessions denken, bei denen 1 normalerweise für die Dauer jeder einzelnen Seitenanforderung erworben und freigegeben wird.

F: Wird ein einfaches Wer zuerst kommt, mahlt zuerst oder brauchen Sie etwas Intelligenteres, das den Hunger verhindern würde?
A: Eine einfache Round-Robin-Verteilung würde ausreichen. Ich gehe davon aus, dass Sie meinen, wenn keine Sitzungen verfügbar sind, Anrufer blockiert werden und auf Freigaben warten. Dies ist nicht wirklich zutreffend, da die Sitzungen von verschiedenen Anrufern gemeinsam genutzt werden können. Mein Ziel ist es, die Nutzung auf mehrere Sitzungen zu verteilen, im Gegensatz zu einer einzelnen Sitzung.

Ich glaube, dies ist wahrscheinlich eine Abweichung von der normalen Verwendung eines Objektpools, weshalb ich diesen Teil ursprünglich weggelassen und geplant habe, nur das Muster anzupassen, um das Teilen von Objekten zu ermöglichen, anstatt jemals eine Hungersituation zuzulassen.

F: Was ist mit Dingen wie Prioritäten, faulem oder eifrigem Laden usw.?
A: Es ist keine Priorisierung erforderlich. Nehmen Sie der Einfachheit halber einfach an, dass ich den Pool verfügbarer Objekte bei der Erstellung des Pools selbst erstellen würde.


1
Können Sie uns etwas über Ihre Anforderungen erzählen? Nicht alle Pools sind gleich. Sind die Objekte teuer in der Herstellung? Werden sie sehr häufig erworben / freigegeben? Wird ein einfaches Wer zuerst kommt, mahlt zuerst oder brauchen Sie etwas Intelligenteres, das den Hunger verhindern würde? Was ist mit Dingen wie Prioritäten, faulem oder eifrigem Laden usw.? Alles, was Sie hinzufügen können, würde uns (oder zumindest mir) helfen, eine gründlichere Antwort zu finden.
Aaronaught

Chris - schauen Sie sich nur Ihren zweiten und dritten Absatz an und fragen Sie sich, ob diese Sitzungen wirklich auf unbestimmte Zeit am Leben bleiben sollten? Es hört sich so an, als würde Ihr Dienstanbieter dies nicht mögen (Sitzungen mit langer Laufzeit). Daher suchen Sie möglicherweise nach einer Pool-Implementierung, die neue Sitzungen nach Bedarf startet und sie herunterfährt, wenn sie nicht verwendet werden (nach einem bestimmten Zeitraum). . Dies ist möglich, aber etwas komplizierter, daher möchte ich dies bestätigen.
Aaronaught

Ich bin mir nicht sicher, ob ich diese robuste Lösung brauche oder noch nicht, da meine Lösung nur hypothetisch ist. Es ist möglich, dass mein Dienstanbieter mich nur anlügt und dass sein Dienst überverkauft ist und lediglich eine Entschuldigung dafür gefunden hat, dem Benutzer die Schuld zu geben.
Chris Marisic

1
Ich denke, der TPL DataFlow BufferBlock macht das meiste, was Sie brauchen.
Spender

1
Das Pooling in Thread-Umgebungen ist ein wiederkehrendes Problem, das durch Entwurfsmuster wie Ressourcenpool und Ressourcen-Cache gelöst wird. Check out Pattern-Oriented Software Architecture, Band 3: Muster für das Ressourcenmanagement für weitere Informationen.
Fuhrmanator

Antworten:


59

Objektpooling in .NET Core

Der Dotnet-Kern verfügt über eine Implementierung von Objektpooling, die der Basisklassenbibliothek (BCL) hinzugefügt wurde. Sie können das ursprüngliche GitHub-Problem hier lesen und den Code für System.Buffers anzeigen . Derzeit ArrayPoolist der einzige verfügbare Typ und wird zum Poolen von Arrays verwendet. Es gibt eine schöne Blog - Post hier .

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

Ein Beispiel für die Verwendung finden Sie in ASP.NET Core. Da es sich in der Dotnet Core-BCL befindet, kann ASP.NET Core seinen Objektpool für andere Objekte wie den JSON-Serializer von Newtonsoft.Json freigeben. In diesem Blogbeitrag finden Sie weitere Informationen dazu, wie Newtonsoft.Json dies tut.

Objektpooling im Microsoft Roslyn C # -Compiler

Der neue Microsoft Roslyn C # -Compiler enthält den ObjectPool- Typ, mit dem häufig verwendete Objekte zusammengefasst werden, die normalerweise neu erstellt und sehr häufig Müll gesammelt werden. Dies reduziert die Menge und Größe der Garbage Collection-Vorgänge, die stattfinden müssen. Es gibt einige verschiedene Unterimplementierungen, die alle ObjectPool verwenden (siehe: Warum gibt es in Roslyn so viele Implementierungen von Object Pooling? ).

1 - SharedPools - Speichert einen Pool von 20 Objekten oder 100, wenn BigDefault verwendet wird.

// 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 und StringBuilderPool - Keine streng getrennten Implementierungen, sondern Wrapper um die oben gezeigte SharedPools-Implementierung, speziell für List und StringBuilder. Dadurch wird der in SharedPools gespeicherte Objektpool wiederverwendet.

// 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 und PooledHashSet - Diese verwenden ObjectPool direkt und haben einen völlig separaten Pool von Objekten. Speichert einen Pool von 128 Objekten.

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

Diese Bibliothek bietet Pooling für MemoryStreamObjekte. Es ist ein Ersatz für System.IO.MemoryStream. Es hat genau die gleiche Semantik. Es wurde von Bing-Ingenieuren entworfen. Lesen Sie den Blog-Beitrag hier oder sehen Sie sich den Code auf GitHub an .

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

Beachten Sie, dass RecyclableMemoryStreamManagerdies einmal deklariert werden sollte und für den gesamten Prozess gültig ist - dies ist der Pool. Es ist vollkommen in Ordnung, mehrere Pools zu verwenden, wenn Sie dies wünschen.


2
Dies ist eine großartige Antwort. Nachdem C # 6 & VS2015 RTM ist, werde ich dies wahrscheinlich zur akzeptierten Antwort machen, da es eindeutig das Beste von allen ist, wenn es so abgestimmt ist, dass es von Rosyln selbst verwendet wird.
Chris Marisic

Ich stimme zu, aber welche Implementierung würden Sie verwenden? Roslyn enthält drei. Siehe den Link zu meiner Frage in der Antwort.
Muhammad Rehan Saeed

1
Es scheint, als ob jeder sehr klar definierte Zwecke hat, viel besser als nur die Wahl einer offenen Einheitsgröße für alle Schuhe.
Chris Marisic

1
@ MuhammadRehanSaeed große Ergänzung mit dem ArrayPool
Chris Marisic

1
Zu sehen, RecyclableMemoryStreamdass dies eine erstaunliche Ergänzung für Optimierungen mit ultrahoher Leistung ist.
Chris Marisic

315

Diese Frage ist aufgrund mehrerer Unbekannter etwas kniffliger als erwartet: Das Verhalten der zu bündelnden Ressource, die erwartete / erforderliche Lebensdauer von Objekten, der wahre Grund, warum der Pool erforderlich ist usw. In der Regel handelt es sich bei Pools um Spezialthreads Pools, Verbindungspools usw. - weil es einfacher ist, einen zu optimieren, wenn Sie genau wissen, was die Ressource tut, und vor allem die Kontrolle darüber haben, wie diese Ressource implementiert wird.

Da es nicht so einfach ist, habe ich versucht, einen ziemlich flexiblen Ansatz anzubieten, mit dem Sie experimentieren und sehen können, was am besten funktioniert. Wir entschuldigen uns im Voraus für den langen Beitrag, aber es gibt viel zu tun, wenn es um die Implementierung eines angemessenen Ressourcenpools für allgemeine Zwecke geht. und ich kratzte wirklich nur an der Oberfläche.

Ein Allzweckpool müsste einige wichtige "Einstellungen" haben, darunter:

  • Strategie zum Laden von Ressourcen - eifrig oder faul;
  • Mechanismus zum Laden von Ressourcen - wie man tatsächlich einen erstellt;
  • Zugriffsstrategie - Sie erwähnen "Round Robin", das nicht so einfach ist, wie es sich anhört. Diese Implementierung kann einen zirkulären Puffer verwenden, der ähnlich , aber nicht perfekt ist, da der Pool keine Kontrolle darüber hat, wann Ressourcen tatsächlich zurückgefordert werden. Andere Optionen sind FIFO und LIFO; FIFO wird eher ein Direktzugriffsmuster haben, aber LIFO erleichtert die Implementierung einer am wenigsten verwendeten Freigabestrategie erheblich (die Ihrer Meinung nach außerhalb des Anwendungsbereichs lag, aber dennoch erwähnenswert ist).

Für den Mechanismus zum Laden von Ressourcen bietet uns .NET bereits eine saubere Abstraktion - Delegaten.

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

Führen Sie dies durch den Konstruktor des Pools und wir sind damit fertig. Die Verwendung eines generischen Typs mit einer new()Einschränkung funktioniert ebenfalls, dies ist jedoch flexibler.


Von den beiden anderen Parametern ist die Zugriffsstrategie das kompliziertere Biest, daher bestand mein Ansatz darin, einen auf Vererbung (Schnittstelle) basierenden Ansatz zu verwenden:

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

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

Das Konzept hier ist einfach: Wir lassen die öffentliche PoolKlasse die allgemeinen Probleme wie die Thread-Sicherheit behandeln, verwenden jedoch für jedes Zugriffsmuster einen anderen "Item Store". LIFO kann leicht durch einen Stapel dargestellt werden, FIFO ist eine Warteschlange, und ich habe eine nicht sehr optimierte, aber wahrscheinlich adäquate Ringpufferimplementierung verwendet, die einen List<T>Indexzeiger verwendet, um ein Round-Robin-Zugriffsmuster zu approximieren.

Alle unten aufgeführten Klassen sind innere Klassen der Pool<T>- dies war eine Stilwahl, aber da diese wirklich nicht außerhalb der Klasse verwendet werden sollen Pool, ist dies am sinnvollsten.

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

Dies sind die offensichtlichen - Stapel und Warteschlange. Ich denke nicht, dass sie wirklich viel Erklärung verdienen. Der Ringpuffer ist etwas komplizierter:

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

Ich hätte verschiedene Ansätze wählen können, aber unter dem Strich sollte auf Ressourcen in derselben Reihenfolge zugegriffen werden, in der sie erstellt wurden. Dies bedeutet, dass wir Verweise auf sie beibehalten, sie jedoch als "in Verwendung" markieren müssen (oder nicht) ). Im schlimmsten Fall ist immer nur ein Steckplatz verfügbar, und für jeden Abruf ist eine vollständige Iteration des Puffers erforderlich. Dies ist schlecht, wenn Sie Hunderte von Ressourcen gepoolt haben und diese mehrmals pro Sekunde erwerben und freigeben. Nicht wirklich ein Problem für einen Pool von 5-10 Elementen, und im typischen Fall, wenn Ressourcen nur wenig genutzt werden, müssen nur ein oder zwei Slots vorgerückt werden.

Denken Sie daran, dass diese Klassen private innere Klassen sind. Deshalb müssen sie nicht viel auf Fehler überprüft werden. Der Pool selbst beschränkt den Zugriff auf sie.

Wenn Sie eine Aufzählung und eine Factory-Methode eingeben, sind wir mit diesem Teil fertig:

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

Das nächste zu lösende Problem ist die Ladestrategie. Ich habe drei Typen definiert:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Die ersten beiden sollten selbsterklärend sein; Der dritte ist eine Art Hybrid, der Ressourcen faul lädt, aber erst dann wieder Ressourcen verwendet, wenn der Pool voll ist. Dies wäre ein guter Kompromiss, wenn Sie möchten, dass der Pool voll ist (wie es sich anhört), aber die Kosten für die tatsächliche Erstellung bis zum ersten Zugriff aufschieben möchten (dh um die Startzeiten zu verbessern).

Die Lademethoden sind wirklich nicht zu kompliziert, jetzt, wo wir die Item-Store-Abstraktion haben:

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

Die obigen Felder sizeund countbeziehen sich auf die maximale Größe des Pools und die Gesamtzahl der Ressourcen, die dem Pool gehören (aber nicht unbedingt verfügbar sind ). AcquireEagerist die einfachste, es wird davon ausgegangen, dass sich ein Artikel bereits im Geschäft befindet - diese Artikel würden bei der Erstellung vorgeladen, dh in der PreloadItemszuletzt gezeigten Methode.

AcquireLazyÜberprüft, ob sich freie Elemente im Pool befinden. Andernfalls wird ein neues erstellt. AcquireLazyExpandingerstellt eine neue Ressource, solange der Pool seine Zielgröße noch nicht erreicht hat. Ich habe versucht, dies zu optimieren, um das Sperren zu minimieren, und ich hoffe, ich habe keine Fehler gemacht (ich habe dies unter Multithread-Bedingungen getestet, aber offensichtlich nicht vollständig).

Sie fragen sich möglicherweise, warum bei keiner dieser Methoden überprüft wird, ob das Geschäft die maximale Größe erreicht hat oder nicht. Ich werde gleich darauf zurückkommen.


Nun zum Pool selbst. Hier ist der vollständige Satz privater Daten, von denen einige bereits angezeigt wurden:

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

Bei der Beantwortung der Frage, die ich im letzten Absatz beschönigt habe - wie man sicherstellt, dass wir die Gesamtzahl der erstellten Ressourcen begrenzen - stellt sich heraus, dass .NET bereits ein perfektes Tool dafür hat, es heißt Semaphore und wurde speziell entwickelt, um eine feste Lösung zu ermöglichen Anzahl der Threads, die auf eine Ressource zugreifen (in diesem Fall ist die "Ressource" der innere Objektspeicher). Da wir keine vollständige Warteschlange für Produzenten / Konsumenten implementieren, ist dies für unsere Anforderungen vollkommen ausreichend.

Der Konstruktor sieht folgendermaßen aus:

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

Sollte hier keine Überraschungen geben. Zu beachten ist nur das Spezialgehäuse für eifriges Laden mit der PreloadItemsbereits zuvor gezeigten Methode.

Da inzwischen fast alles sauber abstrahiert wurde, sind die tatsächlichen Acquireund ReleaseMethoden wirklich sehr einfach:

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

Wie bereits erläutert, verwenden wir das Semaphore, um die Parallelität zu steuern, anstatt den Status des Item-Stores religiös zu überprüfen. Solange erworbene Gegenstände korrekt freigegeben werden, besteht kein Grund zur Sorge.

Last but not least gibt es Aufräumarbeiten:

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

Der Zweck dieser IsDisposedEigenschaft wird in einem Moment klar. Die Hauptmethode Disposebesteht darin, die tatsächlich gepoolten Elemente zu entsorgen, wenn sie implementiert werden IDisposable.


Jetzt können Sie dies im Grunde genommen so wie es ist mit einem try-finallyBlock verwenden, aber ich mag diese Syntax nicht, denn wenn Sie anfangen, gepoolte Ressourcen zwischen Klassen und Methoden weiterzugeben, wird es sehr verwirrend. Es ist möglich , dass die Hauptklasse , die eine Ressource nutzt nicht einmal hat einen Verweis auf den Pool. Es wird wirklich ziemlich chaotisch, daher ist es besser, ein "intelligentes" gepooltes Objekt zu erstellen.

Angenommen, wir beginnen mit der folgenden einfachen Schnittstelle / Klasse:

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

Hier ist unsere vorgetäuschte verfügbare FooRessource, die IFooeinen Boilerplate-Code zum Generieren eindeutiger Identitäten implementiert und enthält. Wir erstellen ein weiteres spezielles, gepooltes Objekt:

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

Dies überträgt nur alle "echten" Methoden auf das Innere IFoo(wir könnten dies mit einer Dynamic Proxy-Bibliothek wie Castle tun, aber darauf werde ich nicht eingehen). Es wird auch ein Verweis auf das Objekt beibehalten, das Pooles erstellt, sodass es sich bei Disposediesem Objekt automatisch wieder in den Pool zurückgibt. Außer wenn der Pool bereits entsorgt wurde - dies bedeutet, dass wir uns im "Bereinigungs" -Modus befinden und in diesem Fall stattdessen die interne Ressource bereinigt wird .


Mit dem obigen Ansatz können wir Code wie folgt schreiben:

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

Das ist eine sehr gute Sache. Es bedeutet , dass der Code, der verwendet die IFoo(im Gegensatz zu dem Code, der es schafft) nicht wirklich bewusst , den Pool sein muß. Sie können Objekte sogar mit Ihrer bevorzugten DI-Bibliothek und als Provider / Factory injizieren .IFooPool<T>


Ich habe den vollständigen Code für das Kopieren und Einfügen in PasteBin eingefügt . Es gibt auch ein kurzes Testprogramm, mit dem Sie mit verschiedenen Lade- / Zugriffsmodi und Multithread-Bedingungen herumspielen können, um sich davon zu überzeugen, dass es threadsicher und nicht fehlerhaft ist.

Lassen Sie mich wissen, wenn Sie Fragen oder Bedenken dazu haben.


62
Eine der vollständigsten, hilfreichsten und interessantesten Antworten, die ich auf SO gelesen habe.
Josh Smeaton

Ich konnte @Josh in Bezug auf diese Antwort nicht mehr zustimmen, insbesondere für den PooledFoo-Teil, da das Freigeben der Objekte immer sehr undicht behandelt zu werden schien und ich mir vorgestellt hatte, dass es am sinnvollsten wäre, die Verwendung verwenden zu können Konstruieren Sie, wie Sie gezeigt haben, dass ich mich einfach nicht hingesetzt habe und versucht habe, das zu bauen, wo Ihre Antwort mir alle Informationen gibt, die ich zur Lösung meines Problems benötigen könnte. Ich denke, für meine spezielle Situation kann ich dies größtenteils ein wenig vereinfachen, da ich die Instanzen zwischen Threads teilen kann und sie nicht wieder in den Pool freigeben muss.
Chris Marisic

Wenn der einfache Ansatz jedoch nicht zuerst funktioniert, habe ich einige Ideen im Kopf, wie ich die Freigabe für meinen Fall intelligent handhaben könnte. Ich denke, am spezifischsten würde ich die Version einrichten, um feststellen zu können, dass diese Sitzung selbst fehlerhaft ist, und um sie zu entsorgen und eine neue in den Pool zu ersetzen. Unabhängig davon, ob dieser Beitrag an dieser Stelle so ziemlich die endgültige Anleitung zum Objektpooling in C # 3.0 ist, freue ich mich darauf, zu sehen, ob jemand mehr Kommentare dazu hat.
Chris Marisic

@Chris: Wenn Sie über WCF-Client-Proxys sprechen, dann habe ich auch ein Muster dafür, obwohl Sie einen Abhängigkeitsinjektor oder Methodenabfangjäger benötigen, um es effektiv zu verwenden. Die DI-Version verwendet den Kernel mit einem benutzerdefinierten Anbieter, um eine neue Version mit fehlerhaftem Fehler zu erhalten. Die Version zum Abfangen von Methoden (meine Präferenz) umschließt lediglich einen vorhandenen Proxy und fügt vor jeder Version eine Fehlerprüfung ein. Ich bin mir nicht sicher, wie einfach es wäre, es in einen Pool wie diesen zu integrieren (habe es nicht wirklich versucht, da ich es gerade geschrieben habe!), Aber es wäre definitiv möglich.
Aaronaught

5
Sehr beeindruckend, obwohl für die meisten Situationen etwas überarbeitet. Ich würde erwarten, dass so etwas Teil eines Rahmens ist.
ChaosPandion

7

So etwas könnte Ihren Bedürfnissen entsprechen.

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

                }
            }
        }
    }
}

Beispiel Verwendung

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
Scratch diesen früheren Kommentar. Ich denke, ich fand es einfach seltsam, weil dieser Pool keine Schwellenwerte zu haben scheint und es vielleicht auch nicht muss, es würde von den Anforderungen abhängen.
Aaronaught

1
@ Aronaught - Ist es wirklich so seltsam? Ich wollte einen leichten Pool erstellen, der genau die Funktionalität bietet, die benötigt wird. Es ist Sache des Kunden, die Klasse ordnungsgemäß zu verwenden.
ChaosPandion

1
+1 für eine sehr einfache Lösung, die an meine Zwecke angepasst werden kann, indem einfach der Hintergrundtyp in eine Liste / HashTable usw. geändert wird und der Zähler so geändert wird, dass ein Rollover durchgeführt wird. Zufällige Frage, wie gehen Sie mit der Verwaltung des Poolobjekts selbst um? Stecken Sie es einfach in einen IOC-Container, der definiert, dass es dort Singleton ist?
Chris Marisic

1
Sollte das nur statisch sein? Aber ich finde es seltsam, dass Sie das in eine finally-Anweisung setzen würden, wenn es eine Ausnahme gibt, wäre es nicht wahrscheinlich, dass das Objekt selbst fehlerhaft ist? Würden Sie das innerhalb der PutMethode behandeln und es der Einfachheit halber weglassen, um zu überprüfen, ob das Objekt fehlerhaft ist, und um eine neue Instanz zu erstellen, die dem Pool hinzugefügt werden soll, anstatt die vorherige einzufügen?
Chris Marisic

1
@ Chris - Ich biete einfach ein einfaches Tool an, das ich in der Vergangenheit als nützlich empfunden habe. Der Rest liegt an dir. Ändern und verwenden Sie den Code nach Belieben.
ChaosPandion

6

Danke für diesen Link. Für diese Implementierung gibt es keine Größenbeschränkung. Wenn Sie also einen Spitzenwert bei der Objekterstellung haben, werden diese Instanzen niemals erfasst und wahrscheinlich erst verwendet, wenn es einen weiteren Spitzenwert gibt. Es ist jedoch sehr einfach und leicht zu verstehen und es wäre nicht schwer, eine maximale Größenbeschränkung hinzuzufügen.
Muhammad Rehan Saeed

Schön und einfach
Daniel de Zwaan

4

Früher stellte Microsoft über Microsoft Transaction Server (MTS) und später COM + ein Framework für das Objekt-Pooling für COM-Objekte bereit. Diese Funktionalität wurde in .NET Framework und jetzt in Windows Communication Foundation auf System.EnterpriseServices übertragen.

Objektpooling in WCF

Dieser Artikel stammt aus .NET 1.1, sollte jedoch in den aktuellen Versionen des Frameworks weiterhin gelten (obwohl WCF die bevorzugte Methode ist).

Objektpooling .NET


+1, um mir zu zeigen, dass die IInstanceProviderSchnittstelle vorhanden ist, da ich dies für meine Lösung implementieren werde. Ich bin immer ein Fan davon, meinen Code hinter einer von Microsoft bereitgestellten Oberfläche zu stapeln, wenn sie eine passende Definition bieten.
Chris Marisic

4

Ich mag die Implementierung von Aronaught sehr - vor allem, weil er das Warten auf die Verfügbarkeit von Ressourcen mithilfe eines Semaphors übernimmt. Es gibt mehrere Ergänzungen, die ich machen möchte:

  1. Änderung sync.WaitOne()zu sync.WaitOne(timeout)und die Timeout als Parameter auf belichten Acquire(int timeout)Methode. Dies würde auch die Behandlung der Bedingung erforderlich machen, wenn der Thread eine Zeitüberschreitung aufweist und darauf wartet, dass ein Objekt verfügbar wird.
  2. Fügen Sie eine Recycle(T item)Methode hinzu, um Situationen zu behandeln, in denen ein Objekt beispielsweise bei einem Fehler recycelt werden muss.

3

Dies ist eine weitere Implementierung mit einer begrenzten Anzahl von Objekten im Pool.

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



Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.