Implementazione del modello di pool di oggetti C #


165

Qualcuno ha una buona risorsa sull'implementazione di una strategia di pool di oggetti condivisi per una risorsa limitata in vena di pool di connessioni SQL? (cioè verrebbe implementato pienamente che è thread-safe).

A seguito della richiesta di chiarimenti di @Aaronaught, l'utilizzo del pool sarebbe per le richieste di bilanciamento del carico a un servizio esterno. Per dirla in uno scenario che sarebbe probabilmente più facile da capire immediatamente in contrapposizione alla mia situtazione diretta. Ho un oggetto sessione che funziona in modo simile ISessionall'oggetto di NHibernate. Che ogni sessione unica gestisca la sua connessione al database. Al momento ho 1 oggetto sessione di lunga durata e sto riscontrando problemi in cui il mio fornitore di servizi sta limitando il mio utilizzo di questa singola sessione.

A causa della loro mancanza di aspettativa che una singola sessione sarebbe trattata come un account di servizio di lunga durata, apparentemente lo trattano come un client che sta martellando il loro servizio. Il che mi porta alla mia domanda qui, invece di avere una singola sessione creerei un pool di sessioni diverse e dividere le richieste fino al servizio su quelle sessioni multiple invece di creare un singolo punto focale come facevo in precedenza.

Spero che questo background offra un valore, ma per rispondere direttamente ad alcune delle tue domande:

D: Gli oggetti sono costosi da creare?
A: Nessun oggetto è un pool di risorse limitate

D: Saranno acquisiti / rilasciati molto frequentemente?
A: Sì, ancora una volta si possono pensare alle NHibernate ISession in cui 1 viene normalmente acquisito e rilasciato per la durata di ogni singola richiesta di pagina.

D: Sarà sufficiente un semplice primo arrivato, o hai bisogno di qualcosa di più intelligente, vale a dire che impedirebbe la fame?
A: Una semplice distribuzione di tipo round robin sarebbe sufficiente, per fame suppongo che tu intenda se non ci sono sessioni disponibili che i chiamanti vengono bloccati in attesa di rilasci. Questo non è realmente applicabile poiché le sessioni possono essere condivise da diversi chiamanti. Il mio obiettivo è distribuire l'utilizzo su più sessioni invece di 1 singola sessione.

Credo che questa sia probabilmente una divergenza rispetto al normale utilizzo di un pool di oggetti, motivo per cui inizialmente ho lasciato fuori questa parte e pianificato solo per adattare il modello per consentire la condivisione di oggetti anziché consentire che si verifichi una situazione di fame.

D: Che dire di priorità, caricamento lento o impaziente, ecc.?
A: Non è prevista alcuna definizione delle priorità, per semplicità, supponiamo che crei il pool di oggetti disponibili al momento della creazione del pool stesso.


1
Puoi parlarci un po 'delle tue esigenze? Non tutti i pool sono uguali. Gli oggetti sono costosi da creare? Saranno acquisiti / rilasciati molto frequentemente? Sarà sufficiente un semplice primo arrivato, o hai bisogno di qualcosa di più intelligente, cioè che impedirebbe la fame? Che dire di cose come priorità, caricamento lento o impaziente, ecc.? Qualsiasi cosa tu possa aggiungere ci aiuterebbe (o almeno io) a trovare una risposta più approfondita.
Aaronaught il

Chris: stai solo guardando il tuo secondo e terzo paragrafo e ti chiedi se queste sessioni debbano davvero essere mantenute in vita a tempo indeterminato? Sembra che questo non piaccia al tuo fornitore di servizi (sessioni di lunga durata), quindi potresti essere in cerca di un'implementazione del pool che giri le nuove sessioni se necessario e le spegne quando non vengono utilizzate (dopo un periodo specificato) . Questo può essere fatto, ma è un po 'più complicato, quindi vorrei confermare.
Aaronaught il

Non sono sicuro di aver bisogno di una soluzione così solida o meno, poiché la mia soluzione è semplicemente ipotetica. È possibile che il mio fornitore di servizi mi stia mentendo e che il loro servizio sia esaurito e abbia semplicemente trovato una scusa per un modo di incolpare l'utente.
Chris Marisic,

1
Penso che TPL DataFlow BufferBlock faccia quasi tutto ciò di cui hai bisogno.
spender

1
Il pooling in ambienti thread è un problema ricorrente, risolto da modelli di progettazione come Resource Pool e Resource Cache. Scopri Pattern-Oriented Software Architecture, Volume 3: Modelli per la gestione delle risorse per ulteriori informazioni.
Fuhrmanator,

Risposte:


59

Pool di oggetti in .NET Core

Il core dotnet ha un'implementazione del pool di oggetti aggiunto alla libreria di classi di base (BCL). Puoi leggere il problema originale di GitHub qui e visualizzare il codice per System.Buffers . Attualmente ArrayPoolè l'unico tipo disponibile e viene utilizzato per raggruppare le matrici. C'è un bel post sul blog qui .

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

Un esempio del suo utilizzo può essere visto in ASP.NET Core. Poiché è nel core BCL dotnet, ASP.NET Core può condividere il pool di oggetti con altri oggetti come il serializzatore JSON di Newtonsoft.Json. Puoi leggere questo post sul blog per ulteriori informazioni su come Newtonsoft.Json lo sta facendo.

Pool di oggetti nel compilatore C # di Microsoft Roslyn

Il nuovo compilatore C # di Microsoft Roslyn contiene il tipo ObjectPool , che viene utilizzato per raggruppare oggetti di uso frequente che normalmente verrebbero recuperati e immondizia raccolta molto spesso. Ciò riduce la quantità e la dimensione delle operazioni di raccolta dei rifiuti che devono avvenire. Esistono diverse implementazioni secondarie tutte che usano ObjectPool (Vedi: Perché ci sono così tante implementazioni di Object Pooling a Roslyn? ).

1 - SharedPools - Memorizza un pool di 20 oggetti o 100 se viene utilizzato 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 e StringBuilderPool - Implementazioni non strettamente separate ma wrapper attorno all'implementazione di SharedPools mostrate sopra specificatamente per List e StringBuilder's. Quindi questo riutilizza il pool di oggetti memorizzati in 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 e PooledHashSet : utilizzano direttamente ObjectPool e dispongono di un pool di oggetti totalmente separato. Memorizza un pool di 128 oggetti.

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

Questa libreria fornisce il pooling per gli MemoryStreamoggetti. È un sostituto drop-in per System.IO.MemoryStream. Ha esattamente la stessa semantica. È stato progettato dagli ingegneri Bing. Leggi il post sul blog qui o vedi il codice su 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); 
}

Nota che RecyclableMemoryStreamManagerdovrebbe essere dichiarato una volta e vivrà per l'intero processo: questo è il pool. Va benissimo usare più pool se lo desideri.


2
Questa è un'ottima risposta Dopo che C # 6 e VS2015 saranno RTM, probabilmente lo farò la risposta accettata in quanto è chiaramente il migliore di tutti se è così ottimizzato da essere utilizzato dallo stesso Rosyln.
Chris Marisic,

Sono d'accordo ma quale implementazione useresti? Roslyn ne contiene tre. Vedi il link alla mia domanda nella risposta.
Muhammad Rehan Saeed,

1
Sembra che ognuno di essi abbia scopi ben definiti, molto meglio della sola scelta di una taglia aperta a tutte le scarpe.
Chris Marisic,

1
@MuhammadRehanSaeed grande aggiunta con ArrayPool
Chris Marisic

1
Vedendo RecyclableMemoryStreamche è un'aggiunta straordinaria per ottimizzazioni ad altissime prestazioni.
Chris Marisic,

315

Questa domanda è un po 'più complicata di quanto ci si potrebbe aspettare a causa di diverse incognite: il comportamento della risorsa in pool, la durata prevista / richiesta degli oggetti, la vera ragione per cui è richiesto il pool, ecc. In genere i pool sono per scopi speciali - thread pool, pool di connessioni, ecc. - perché è più facile ottimizzarne uno quando si conosce esattamente cosa fa la risorsa e, cosa più importante, si ha il controllo su come tale risorsa viene implementata.

Dal momento che non è così semplice, quello che ho provato a fare è offrire un approccio abbastanza flessibile che puoi sperimentare e vedere cosa funziona meglio. Ci scusiamo in anticipo per il lungo post, ma c'è molto terreno da affrontare quando si tratta di implementare un discreto pool di risorse per scopi generici. e sto davvero solo grattando la superficie.

Un pool di uso generale dovrebbe avere alcune "impostazioni" principali, tra cui:

  • Strategia di caricamento delle risorse: desiderosa o pigra;
  • Meccanismo di caricamento delle risorse - come costruirne effettivamente uno;
  • Strategia di accesso - menzioni "round robin" che non è così semplice come sembra; questa implementazione può utilizzare un buffer circolare simile , ma non perfetto, poiché il pool non ha alcun controllo su quando le risorse vengono effettivamente recuperate. Altre opzioni sono FIFO e LIFO; FIFO avrà più di un modello di accesso casuale, ma LIFO semplifica notevolmente l'implementazione di una strategia di liberazione utilizzata di recente (che hai detto non rientra nell'ambito di applicazione, ma vale comunque la pena menzionarla).

Per il meccanismo di caricamento delle risorse, .NET ci offre già un'astrazione chiara: i delegati.

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

Passa attraverso il costruttore del pool e abbiamo finito. L'uso di un tipo generico con un new()vincolo funziona anche, ma questo è più flessibile.


Degli altri due parametri, la strategia di accesso è la bestia più complicata, quindi il mio approccio è stato quello di utilizzare un approccio basato sull'ereditarietà (interfaccia):

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

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

Il concetto qui è semplice: lasceremo che la Poolclasse pubblica gestisca i problemi comuni come la sicurezza dei thread, ma utilizzeremo un "archivio oggetti" diverso per ogni modello di accesso. LIFO è facilmente rappresentato da uno stack, FIFO è una coda e ho usato un'implementazione del buffer circolare non molto ottimizzata ma probabilmente adeguata utilizzando un List<T>puntatore e indice per approssimare un modello di accesso round-robin.

Tutte le classi sottostanti sono classi interne della Pool<T>- questa è stata una scelta di stile, ma dal momento che queste non sono pensate per essere utilizzate al di fuori della Pool, ha più senso.

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

Questi sono quelli ovvi: pila e coda. Non credo che meritino davvero molte spiegazioni. Il buffer circolare è un po 'più complicato:

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

Avrei potuto scegliere una serie di approcci diversi, ma la linea di fondo è che le risorse dovrebbero essere accessibili nello stesso ordine in cui sono state create, il che significa che dobbiamo mantenere i riferimenti a loro ma contrassegnarli come "in uso" (o no ). Nel peggiore dei casi, è sempre disponibile solo uno slot, che richiede un'iterazione completa del buffer per ogni recupero. Ciò è negativo se si hanno in comune centinaia di risorse e le si stanno acquisendo e rilasciando più volte al secondo; non è davvero un problema per un pool di 5-10 elementi e, nel caso tipico , in cui le risorse sono leggermente utilizzate, deve solo avanzare di uno o due slot.

Ricorda, queste classi sono classi interne private - ecco perché non hanno bisogno di molto controllo degli errori, il pool stesso limita l'accesso ad esse.

Aggiungi un elenco e un metodo factory e abbiamo finito con questa parte:

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

Il prossimo problema da risolvere è il caricamento della strategia. Ho definito tre tipi:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

I primi due dovrebbero essere autoesplicativi; il terzo è una specie di ibrido, carica le risorse in modo pigro ma in realtà non inizia a riutilizzare alcuna risorsa fino a quando il pool non è pieno. Questo sarebbe un buon compromesso se si desidera che il pool sia pieno (come sembra che si faccia) ma si desidera rinviare le spese di creazione effettiva fino al primo accesso (ovvero per migliorare i tempi di avvio).

I metodi di caricamento non sono davvero troppo complicati, ora che abbiamo l'astrazione dell'archivio articoli:

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

I campi sizee countsopra si riferiscono alla dimensione massima del pool e al numero totale di risorse possedute dal pool (ma non necessariamente disponibili ), rispettivamente. AcquireEagerè il più semplice, presuppone che un articolo sia già nel negozio - questi elementi sarebbero precaricati durante la costruzione, cioè nel PreloadItemsmetodo mostrato per ultimo.

AcquireLazycontrolla se ci sono articoli gratuiti nel pool e, in caso contrario, ne crea uno nuovo. AcquireLazyExpandingcreerà una nuova risorsa purché il pool non abbia ancora raggiunto la dimensione target. Ho cercato di ottimizzare questo per ridurre al minimo il blocco, e spero di non aver fatto errori (io ho testato questo in condizioni di multi-threaded, ma ovviamente non esaustivo).

Ci si potrebbe chiedere perché nessuno di questi metodi si preoccupi di verificare se il negozio ha raggiunto o meno la dimensione massima. Ci arrivo tra un momento.


Ora per la piscina stessa. Ecco la serie completa di dati privati, alcuni dei quali sono già stati mostrati:

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

Rispondendo alla domanda che ho ripassato nell'ultimo paragrafo - come garantire che limitiamo il numero totale di risorse create - si scopre che .NET ha già uno strumento perfettamente valido per questo, si chiama Semaphore ed è progettato specificamente per consentire un numero di thread di accesso a una risorsa (in questo caso la "risorsa" è l'archivio oggetti interno). Poiché non stiamo implementando una coda produttore / consumatore completa, questo è perfettamente adeguato alle nostre esigenze.

Il costruttore si presenta così:

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

Non dovrebbero esserci sorprese qui. L'unica cosa da notare è l'involucro speciale per il caricamento desideroso, utilizzando il PreloadItemsmetodo già mostrato in precedenza.

Poiché ormai quasi tutto è stato chiaramente sottratto, i metodi Acquiree i Releasemetodi sono davvero molto semplici:

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

Come spiegato in precedenza, stiamo usando il Semaphoreper controllare la concorrenza invece di controllare religiosamente lo stato del negozio di articoli. Finché gli oggetti acquisiti vengono rilasciati correttamente, non c'è nulla di cui preoccuparsi.

Ultimo ma non meno importante, c'è la pulizia:

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

Lo scopo di quella IsDisposedproprietà diventerà chiaro in un momento. Tutto il Disposemetodo principale è davvero quello di eliminare gli oggetti in pool reali se implementati IDisposable.


Ora puoi fondamentalmente usare questo così com'è, con un try-finallyblocco, ma non mi piace quella sintassi, perché se inizi a passare risorse raggruppate tra classi e metodi, diventerà molto confuso. E 'possibile che la classe principale che utilizza una risorsa non ha nemmeno avere un riferimento per la piscina. Diventa davvero piuttosto disordinato, quindi un approccio migliore è quello di creare un oggetto pool "intelligente".

Diciamo che iniziamo con la seguente semplice interfaccia / classe:

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

Ecco la nostra finta Foorisorsa usa IFooe getta che implementa e ha un po 'di codice boilerplate per generare identità univoche. Quello che facciamo è creare un altro oggetto speciale, raggruppato:

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

Questo inoltra semplicemente tutti i metodi "reali" al suo interno IFoo(potremmo farlo con una libreria di proxy dinamici come Castle, ma non entrerò in quello). Mantiene anche un riferimento a Poolciò che lo crea, in modo che quando Disposequesto oggetto viene rilasciato automaticamente nel pool. Tranne quando il pool è già stato eliminato, ciò significa che siamo in modalità "cleanup" e in questo caso pulisce invece la risorsa interna .


Usando l'approccio sopra, arriviamo a scrivere codice in questo modo:

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

Questa è una molto buona cosa per essere in grado di fare. Significa che il codice che utilizza il IFoo(al contrario del codice che lo crea) non deve effettivamente essere a conoscenza del pool. Puoi anche iniettare IFoo oggetti usando la tua libreria DI preferita e Pool<T>come provider / factory.


Ho inserito il codice completo su PasteBin per il tuo divertimento con il copia e incolla. C'è anche un breve programma di test che puoi usare per giocare con diverse modalità di caricamento / accesso e condizioni multithread, per assicurarti che sia thread-safe e non buggy.

Fammi sapere se hai domande o dubbi in merito.


62
Una delle risposte più complete, utili e interessanti che ho letto su SO.
Josh Smeaton,

Non potrei essere più d'accordo con @Josh riguardo a questa risposta, specialmente per la parte PooledFoo in quanto il rilascio degli oggetti sembrava essere sempre gestito in modo molto permissivo e avevo immaginato che avrebbe molto senso avere la possibilità di usare costruire come hai mostrato, non mi ero mai seduto e ho provato a costruirlo dove la tua risposta mi dava tutte le informazioni di cui potevo avere bisogno per risolvere il mio problema. Penso che per la mia situazione specifica sarò in grado di semplificare un po 'principalmente perché posso condividere le istanze tra i thread e non ho bisogno di rilasciarle di nuovo nel pool.
Chris Marisic,

Tuttavia, se l'approccio semplice non funziona per primo, ho alcune idee in testa su come potrei gestire in modo intelligente il rilascio per il mio caso. Penso che in particolare definirei il rilascio per essere in grado di determinare la sessione stessa difettosa e di eliminarla e sostituirne una nuova nel pool. Indipendentemente da questo post a questo punto è praticamente la guida definitiva sul pooling di oggetti in C # 3.0, non vedo l'ora di vedere se qualcun altro ha più commenti su questo.
Chris Marisic,

@Chris: Se stai parlando di proxy client WCF, ho anche uno schema per quello, anche se hai bisogno di un iniettore di dipendenza o di un intercettore di metodo per usarlo in modo efficace. La versione DI utilizza il kernel con un provider personalizzato per ottenere una versione nuova in caso di errore, la versione di intercettazione del metodo (la mia preferenza) avvolge solo un proxy esistente e inserisce un controllo degli errori prima di ciascuno. Non sono sicuro di quanto sarebbe facile integrarlo in un pool come questo (non ci ho davvero provato, dato che l'ho appena scritto!), Ma sarebbe sicuramente possibile.
Aaronaught,

5
Molto impressionante, anche se un po 'troppo ingegnerizzato per la maggior parte delle situazioni. Mi aspetto che qualcosa del genere faccia parte di un quadro.
ChaosPandion

7

Qualcosa del genere potrebbe soddisfare le tue esigenze.

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

                }
            }
        }
    }
}

Esempio di utilizzo

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
Gratta quel commento precedente. Penso di averlo trovato strano perché questo pool non sembra avere soglie e forse non è necessario, dipenderà dai requisiti.
Aaronaught il

1
@Aaronaught - È davvero così strano? Volevo creare un pool leggero che offre solo le funzionalità necessarie. Spetta al cliente utilizzare correttamente la classe.
ChaosPandion

1
+1 per una soluzione molto semplice che può essere adattata ai miei scopi semplicemente cambiando il tipo di supporto in Elenco / HashTable ecc. E cambiando il contatore per il roll over. Domanda casuale come si gestisce la gestione dell'oggetto pool stesso? Lo metti semplicemente in un contenitore IOC che lo definisce singleton lì?
Chris Marisic,

1
Dovrebbe essere statico di sola lettura? Ma trovo strano che tu abbia messo dentro un'istruzione finally, se c'è un'eccezione non sarebbe probabile che l'oggetto stesso sia in errore? Lo gestiresti all'interno del Putmetodo e lo tralasciassi per semplicità un tipo di controllo per verificare se l'oggetto è difettoso e per creare una nuova istanza da aggiungere al pool invece di inserire il precedente?
Chris Marisic,

1
@ Chris - Sto semplicemente offrendo uno strumento semplice che ho trovato utile in passato. Il resto sta a voi. Modifica e usa il codice come ritieni opportuno.
ChaosPandion,

6

Grazie per quel link. Non ci sono limiti di dimensione per questa implementazione, quindi se hai un picco nella creazione di oggetti, quelle istanze non verranno mai raccolte e probabilmente non verranno mai utilizzate fino a quando non ci sarà un altro picco. È molto semplice e facile da capire e non sarebbe difficile aggiungere un limite di dimensione massima.
Muhammad Rehan Saeed,

Simpatico e semplice
Daniel de Zwaan,

4

In passato Microsoft ha fornito un framework tramite Microsoft Transaction Server (MTS) e successivamente COM + per eseguire il pool di oggetti per gli oggetti COM. Tale funzionalità è stata trasferita a System.EnterpriseServices in .NET Framework e ora in Windows Communication Foundation.

Pool di oggetti in WCF

Questo articolo proviene da .NET 1.1 ma dovrebbe comunque essere applicato nelle versioni correnti di Framework (anche se WCF è il metodo preferito).

Pool di oggetti .NET


+1 per avermi mostrato che l' IInstanceProviderinterfaccia esiste poiché lo implementerò per la mia soluzione. Sono sempre un fan dello stacking del mio codice dietro un'interfaccia fornita da Microsoft quando forniscono una definizione adeguata.
Chris Marisic,

4

Mi piace molto l'implementazione di Aronaught, soprattutto perché gestisce l'attesa delle risorse per renderle disponibili attraverso l'uso di un semaforo. Ci sono diverse aggiunte che vorrei fare:

  1. Passare sync.WaitOne()a sync.WaitOne(timeout)ed esporre il timeout come parametro sul Acquire(int timeout)metodo. Ciò richiederebbe anche la gestione della condizione quando il thread scade in attesa che un oggetto diventi disponibile.
  2. Aggiungi un Recycle(T item)metodo per gestire le situazioni in cui un oggetto deve essere riciclato quando si verifica un errore, ad esempio.

3

Questa è un'altra implementazione, con un numero limitato di oggetti nel 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();
        }
    }
}



Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.