Implementación de C # Object Pooling Pattern

165

¿Alguien tiene un buen recurso para implementar una estrategia de agrupación de objetos compartidos para un recurso limitado en vena de la agrupación de conexiones SQL? (es decir, se implementaría completamente de forma segura para subprocesos).

Para realizar un seguimiento de la solicitud de aclaración de @Aaronaught, el uso del grupo sería para solicitudes de equilibrio de carga a un servicio externo. Para ponerlo en un escenario que probablemente sería más fácil de entender de inmediato en lugar de mi situación directa. Tengo un objeto de sesión que funciona de manera similar al ISessionobjeto de NHibernate. Que cada sesión única gestione su conexión a la base de datos. Actualmente tengo 1 objeto de sesión de larga ejecución y encuentro problemas en los que mi proveedor de servicios limita la velocidad de mi uso de esta sesión individual.

Debido a su falta de expectativas de que una sola sesión sea tratada como una cuenta de servicio de larga duración, aparentemente la tratan como un cliente que está afectando su servicio. Lo que me lleva a mi pregunta aquí, en lugar de tener una sesión individual, crearía un grupo de sesiones diferentes y dividiría las solicitudes al servicio en esas sesiones múltiples en lugar de crear un único punto focal como lo estaba haciendo anteriormente.

Esperemos que ese fondo ofrezca algún valor, pero para responder directamente algunas de sus preguntas:

P: ¿Los objetos son caros de crear?
R: Ningún objeto es un grupo de recursos limitados.

P: ¿Se adquirirán / liberarán con mucha frecuencia?
R: Sí, una vez más se les puede pensar en NHibernate ISessions, donde generalmente se adquiere y libera 1 durante la duración de cada solicitud de página.

P: ¿Será suficiente un simple orden de llegada o necesita algo más inteligente, es decir, que evitaría la inanición?
R: Una distribución simple de tipo round robin sería suficiente, por inanición, supongo que quiere decir que si no hay sesiones disponibles, las personas que llaman se bloquean en espera de lanzamientos. Esto no es realmente aplicable ya que las sesiones pueden ser compartidas por diferentes personas que llaman. Mi objetivo es distribuir el uso en varias sesiones en lugar de una sola sesión.

Creo que esto es probablemente una divergencia del uso normal de un grupo de objetos, por lo que originalmente omití esta parte y planeé adaptar el patrón para permitir el intercambio de objetos en lugar de permitir que ocurra una situación de hambre.

P: ¿Qué pasa con cosas como prioridades, carga perezosa o ansiosa, etc.?
R: No hay una priorización involucrada, por simplicidad, solo asuma que crearía el grupo de objetos disponibles en la creación del grupo mismo.

Chris Marisic
fuente
1
¿Puede contarnos un poco sobre sus requisitos? No todas las agrupaciones son iguales. ¿Los objetos son caros de crear? ¿Serán adquiridos / liberados con mucha frecuencia? ¿Será suficiente un simple orden de llegada o necesita algo más inteligente, es decir, que evitaría la inanición? ¿Qué pasa con cosas como prioridades, carga perezosa o ansiosa, etc.? Cualquier cosa que pueda agregar nos ayudaría (o al menos a mí) a encontrar una respuesta más completa.
Aaronaught
Chris: ¿solo estás viendo tus párrafos segundo y tercero y te preguntas si estas sesiones deberían mantenerse vivas indefinidamente? Parece que eso es lo que no le gusta a su proveedor de servicios (sesiones de larga duración), por lo que podría estar buscando una implementación de grupo que acelere las nuevas sesiones según sea necesario y las cierre cuando no estén en uso (después de un período específico) . Esto se puede hacer, pero es un poco más complicado, así que me gustaría confirmarlo.
Aaronaught
No estoy seguro de si necesito una solución tan sólida o no, ya que mi solución es meramente hipotética. Es posible que mi proveedor de servicios solo me esté mintiendo y que su servicio se venda en exceso y simplemente haya encontrado una excusa para culpar al usuario.
Chris Marisic
1
Creo que TPL DataFlow BufferBlock hace la mayor parte de lo que necesita.
Spender
1
La agrupación en entornos con subprocesos es un problema recurrente, resuelto mediante patrones de diseño como Resource Pool y Resource Cache. Consulte Arquitectura de software orientada a patrones, Volumen 3: Patrones para la gestión de recursos para obtener más información.
Fuhrmanator

Respuestas:

59

Agrupación de objetos en .NET Core

El núcleo dotnet tiene una implementación de agrupación de objetos añadida a la biblioteca de clases base (BCL). Puede leer el problema original de GitHub aquí y ver el código de System.Buffers . Actualmente, ArrayPooles el único tipo disponible y se utiliza para agrupar matrices. Hay una buena publicación de blog aquí .

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 ejemplo de su uso se puede ver en ASP.NET Core. Debido a que está en el BCL de dotnet core, ASP.NET Core puede compartir su grupo de objetos con otros objetos como el serializador JSON de Newtonsoft.Json. Puede leer esta publicación de blog para obtener más información sobre cómo Newtonsoft.Json está haciendo esto.

Agrupación de objetos en el compilador de Microsoft Roslyn C #

El nuevo compilador de Microsoft Roslyn C # contiene el tipo ObjectPool , que se usa para agrupar objetos de uso frecuente que normalmente se actualizarían y la basura se recogería con mucha frecuencia. Esto reduce la cantidad y el tamaño de las operaciones de recolección de basura que tienen que suceder. Existen algunas subimplementaciones diferentes, todas utilizando ObjectPool (consulte: ¿Por qué hay tantas implementaciones de Object Pooling en Roslyn? ).

1 - SharedPools : almacena un grupo de 20 objetos o 100 si se usa 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 y StringBuilderPool : no son implementaciones estrictamente separadas, sino envoltorios alrededor de la implementación SharedPools que se muestra arriba específicamente para List y StringBuilder's. Entonces, esto reutiliza el grupo de objetos almacenados en 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 y PooledHashSet : utilizan ObjectPool directamente y tienen un grupo de objetos totalmente separado. Almacena un grupo de 128 objetos.

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

Esta biblioteca proporciona agrupación de MemoryStreamobjetos. Es un reemplazo directo para System.IO.MemoryStream. Tiene exactamente la misma semántica. Fue diseñado por los ingenieros de Bing. Lea la publicación del blog aquí o vea el código en 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); 
}

Tenga en cuenta que RecyclableMemoryStreamManagerdebe declararse una vez y vivirá durante todo el proceso; este es el grupo. Está perfectamente bien usar varias piscinas si lo desea.

Muhammad Rehan Saeed
fuente
2
Esta es una respuesta genial. Después de que C # 6 y VS2015 sea RTM, probablemente haré de esta la respuesta aceptada, ya que es claramente la mejor de todas si está tan ajustada que es utilizada por Rosyln.
Chris Marisic
Estoy de acuerdo, pero ¿qué implementación usarías? Roslyn contiene tres. Vea el enlace a mi pregunta en la respuesta.
Muhammad Rehan Saeed
1
Parece que cada uno tiene propósitos muy claramente definidos, mucho mejor que solo la elección de un zapato abierto de talla única para todos los zapatos.
Chris Marisic
1
@MuhammadRehanSaeed gran adición con el ArrayPool
Chris Marisic
1
Ver RecyclableMemoryStreameso es una adición increíble para optimizaciones de rendimiento ultra alto.
Chris Marisic
315

Esta pregunta es un poco más complicada de lo que uno podría esperar debido a varias incógnitas: el comportamiento del recurso que se está agrupando, la vida útil esperada / requerida de los objetos, la razón real por la que se requiere el grupo, etc. Por lo general, los grupos son de propósito especial - hilo grupos, grupos de conexión, etc., porque es más fácil optimizar uno cuando se sabe exactamente qué hace el recurso y, lo que es más importante, tiene control sobre cómo se implementa ese recurso.

Como no es tan simple, lo que he intentado hacer es ofrecer un enfoque bastante flexible con el que pueda experimentar y ver qué funciona mejor. Disculpas de antemano por la larga publicación, pero hay mucho terreno por cubrir cuando se trata de implementar un grupo de recursos decente de propósito general. y realmente solo estoy rascando la superficie.

Un grupo de uso general tendría que tener algunas "configuraciones" principales, que incluyen:

  • Estrategia de carga de recursos: ansiosa o perezosa;
  • Mecanismo de carga de recursos : cómo construir uno realmente;
  • Estrategia de acceso: usted menciona "round robin", que no es tan sencillo como parece; Esta implementación puede usar un búfer circular que es similar , pero no perfecto, porque el grupo no tiene control sobre cuándo se recuperan los recursos. Otras opciones son FIFO y LIFO; FIFO tendrá más de un patrón de acceso aleatorio, pero LIFO hace que sea significativamente más fácil implementar una estrategia de liberación menos utilizada recientemente (que usted dijo que estaba fuera de alcance, pero aún vale la pena mencionarla).

Para el mecanismo de carga de recursos, .NET ya nos da una abstracción limpia: delegados.

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

Pase esto a través del constructor de la piscina y ya estamos listos para eso. El uso de un tipo genérico con una new()restricción también funciona, pero esto es más flexible.


De los otros dos parámetros, la estrategia de acceso es la bestia más complicada, por lo que mi enfoque fue utilizar un enfoque basado en herencia (interfaz):

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

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

El concepto aquí es simple: dejaremos que la Poolclase pública maneje los problemas comunes como la seguridad de subprocesos, pero usaremos un "almacén de elementos" diferente para cada patrón de acceso. LIFO se representa fácilmente mediante una pila, FIFO es una cola y he utilizado una implementación de búfer circular no muy optimizada pero probablemente adecuada utilizando un List<T>puntero e índice para aproximar un patrón de acceso de round-robin.

Todas las clases a continuación son clases internas de Pool<T>: esta fue una elección de estilo, pero dado que realmente no están destinadas a usarse fuera del Pool, tiene más sentido.

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

Estos son los obvios: apilar y hacer cola. No creo que realmente justifiquen mucha explicación. El búfer circular es un poco más complicado:

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

Podría haber elegido varios enfoques diferentes, pero la conclusión es que se debe acceder a los recursos en el mismo orden en que fueron creados, lo que significa que tenemos que mantener referencias a ellos pero marcarlos como "en uso" (o no ) En el peor de los casos, solo hay una ranura disponible, y se necesita una iteración completa del búfer para cada búsqueda. Esto es malo si tiene cientos de recursos agrupados y los adquiere y libera varias veces por segundo; no es realmente un problema para un grupo de 5-10 elementos, y en el caso típico , donde los recursos se usan ligeramente, solo tiene que avanzar uno o dos espacios.

Recuerde, estas clases son clases internas privadas, es por eso que no necesitan una gran cantidad de verificación de errores, el grupo en sí restringe el acceso a ellas.

Agregue una enumeración y un método de fábrica y hemos terminado con esta 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);
        }
    }

El siguiente problema a resolver es la estrategia de carga. He definido tres tipos:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Los dos primeros deben explicarse por sí mismos; el tercero es una especie de híbrido, carga los recursos de forma diferida pero en realidad no comienza a reutilizar ningún recurso hasta que el grupo esté lleno. Esto sería una buena compensación si desea que el grupo esté lleno (lo que parece que sí) pero desea diferir el gasto de crearlos hasta el primer acceso (es decir, para mejorar los tiempos de inicio).

Los métodos de carga realmente no son demasiado complicados, ahora que tenemos la abstracción de la tienda de artículos:

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

Los campos sizey countarriba se refieren al tamaño máximo del grupo y al número total de recursos que posee el grupo (pero no necesariamente disponibles ), respectivamente. AcquireEageres el más simple, supone que un artículo ya está en la tienda; estos artículos se cargarán previamente en la construcción, es decir, en el PreloadItemsmétodo que se muestra al final.

AcquireLazycomprueba si hay elementos gratuitos en el grupo y, si no, crea uno nuevo. AcquireLazyExpandingcreará un nuevo recurso siempre que el grupo aún no haya alcanzado su tamaño objetivo. He intentado optimizar esto para minimizar el bloqueo, y espero no haber cometido ningún error (lo he probado en condiciones de subprocesos múltiples, pero obviamente no de forma exhaustiva).

Tal vez se pregunte por qué ninguno de estos métodos se molesta en verificar si la tienda ha alcanzado el tamaño máximo. Llegaré a eso en un momento.


Ahora para la piscina en sí. Aquí está el conjunto completo de datos privados, algunos de los cuales ya se han mostrado:

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

Respondiendo a la pregunta que pasé por alto en el último párrafo: cómo asegurarnos de limitar el número total de recursos creados, resulta que .NET ya tiene una herramienta perfectamente buena para eso, se llama Semaphore y está diseñado específicamente para permitir un número de hilos de acceso a un recurso (en este caso, el "recurso" es el almacén de elementos interno). Como no estamos implementando una cola completa de productores / consumidores, esto es perfectamente adecuado para nuestras necesidades.

El constructor se ve así:

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

No debería haber sorpresas aquí. Lo único a tener en cuenta es la carcasa especial para la carga ansiosa, utilizando el PreloadItemsmétodo que ya se mostró anteriormente.

Dado que casi todo se ha abstraído limpiamente por ahora, lo real Acquirey los Releasemétodos son realmente muy sencillos:

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

Como se explicó anteriormente, estamos usando el Semaphorepara controlar la concurrencia en lugar de verificar religiosamente el estado de la tienda de artículos. Mientras los artículos adquiridos se liberen correctamente, no hay nada de qué preocuparse.

Por último, pero no menos importante, hay limpieza:

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

El propósito de esa IsDisposedpropiedad quedará claro en un momento. Todo lo que el Disposemétodo principal realmente hace es deshacerse de los elementos agrupados reales si se implementan IDisposable.


Ahora, básicamente, puede usar esto tal cual, con un try-finallybloque, pero no me gusta esa sintaxis, porque si comienza a distribuir recursos agrupados entre clases y métodos, se volverá muy confuso. Es posible que la clase principal que utiliza un recurso ni siquiera tenga una referencia al grupo. Realmente se vuelve bastante desordenado, por lo que un mejor enfoque es crear un objeto agrupado "inteligente".

Digamos que comenzamos con la siguiente interfaz / clase simple:

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

Aquí está nuestro Foorecurso desechable que se implementa IFooy tiene un código repetitivo para generar identidades únicas. Lo que hacemos es crear otro objeto agrupado especial:

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

Esto simplemente representa todos los métodos "reales" en su interior IFoo(podríamos hacer esto con una biblioteca de proxy dinámico como Castle, pero no voy a entrar en eso). También mantiene una referencia a lo Poolque lo crea, de modo que cuando Disposeeste objeto, se libera automáticamente al grupo. Excepto cuando el grupo ya se ha eliminado, esto significa que estamos en modo de "limpieza" y, en este caso, en realidad limpia el recurso interno .


Usando el enfoque anterior, podemos escribir código como este:

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

Esto es algo muy bueno para poder hacer. Significa que el código que usa el IFoo(en oposición al código que lo crea) en realidad no necesita conocer el grupo. Incluso puede inyectar IFoo objetos usando su biblioteca DI favorita y Pool<T>como proveedor / fábrica.


Puse el código completo en PasteBin para que disfrutes al copiar y pegar. También hay un breve programa de prueba que puede usar para jugar con diferentes modos de carga / acceso y condiciones multiproceso, para asegurarse de que es seguro para subprocesos y no tiene errores.

Avíseme si tiene alguna pregunta o inquietud sobre esto.

Aaronaught
fuente
62
Una de las respuestas más completas, útiles e interesantes que he leído en SO.
Josh Smeaton el
No podría estar más de acuerdo con @Josh acerca de esta respuesta, especialmente para la parte de PooledFoo, ya que la liberación de los objetos siempre parecía manejarse de una manera muy permeable e imaginé que tendría más sentido tener el uso posible. construya como usted mostró que simplemente no me había sentado y traté de construir eso donde su respuesta me da toda la información que podría necesitar para resolver mi problema. Creo que, para mi situación específica, podré simplificar esto un poco en su mayoría, ya que puedo compartir las instancias entre los hilos y no es necesario volver a lanzarlas al grupo.
Chris Marisic
Sin embargo, si el enfoque simple no funciona primero, tengo algunas ideas en mi cabeza sobre cómo podría manejar inteligentemente la liberación para mi caso. Creo que más específicamente establecería el lanzamiento para poder determinar qué sesión falló en sí misma y deshacerme de ella y reemplazar una nueva en el grupo. Independientemente de que esta publicación en este punto sea más o menos la guía definitiva sobre la agrupación de objetos en C # 3.0, espero ver si alguien más tiene más comentarios al respecto.
Chris Marisic
@Chris: Si estás hablando de proxies de clientes WCF, entonces también tengo un patrón para eso, aunque necesitas un inyector de dependencia o un interceptor de métodos para usarlo de manera efectiva. La versión DI utiliza el núcleo con un proveedor personalizado para obtener una versión nueva si falla, la versión de intercepción del método (mi preferencia) simplemente envuelve un proxy existente e inserta una verificación de falla antes de cada uno. No estoy seguro de lo fácil que sería integrarlo en un grupo como este (¡realmente no lo he intentado, ya que acabo de escribir esto!), Pero definitivamente sería posible.
Aaronaught
55
Muy impresionante, aunque un poco sobre diseñado para la mayoría de las situaciones. Esperaría que algo como esto sea parte de un marco.
ChaosPandion
7

Algo como esto podría satisfacer sus necesidades.

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

                }
            }
        }
    }
}

Ejemplo de uso

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
fuente
1
Rasca ese comentario anterior. Creo que me pareció extraño porque este grupo no parece tener ningún umbral, y tal vez no sea necesario, dependería de los requisitos.
Aaronaught
1
@Aaronaught - ¿Es realmente tan extraño? Quería crear un grupo ligero que ofreciera la funcionalidad necesaria. Depende del cliente usar la clase correctamente.
ChaosPandion
1
+1 para una solución muy simple que se puede adaptar a mis propósitos simplemente cambiando el tipo de respaldo para ser una Lista / Tabla de Hash, etc. y cambiando el contador para pasar. Pregunta aleatoria ¿cómo maneja la administración del objeto del grupo en sí? ¿Lo pegas en un contenedor IOC definiéndolo como singleton allí?
Chris Marisic
1
¿Debería ser de solo lectura estática? Pero me parece extraño que lo pongas dentro de una declaración final, si hay una excepción, ¿no sería probable que el objeto en sí sea defectuoso? ¿Manejaría eso dentro del Putmétodo y lo dejaría fuera para simplificar algún tipo de verificación de si el objeto tiene fallas y para crear una nueva instancia para agregar al grupo en lugar de insertar la anterior?
Chris Marisic
1
@ Chris - Simplemente estoy ofreciendo una herramienta simple que he encontrado útil en el pasado. El resto depende de usted. Modifique y use el código como mejor le parezca.
ChaosPandion
6

Muestra de MSDN: Cómo: crear un grupo de objetos mediante un ConcurrentBag

Thomas Mutzl
fuente
Gracias por ese enlace. No hay límite de tamaño para esta implementación, por lo que si tiene un pico en la creación de objetos, esas instancias nunca se recopilarán y probablemente nunca se usarán hasta que haya otro pico. Sin embargo, es muy simple y fácil de entender y no sería difícil agregar un límite de tamaño máximo.
Muhammad Rehan Saeed
Agradable y simple
Daniel de Zwaan
4

En el pasado, Microsoft proporcionó un marco a través de Microsoft Transaction Server (MTS) y más tarde COM + para hacer la agrupación de objetos para objetos COM. Esa funcionalidad se transfirió a System.EnterpriseServices en .NET Framework y ahora en Windows Communication Foundation.

Agrupación de objetos en WCF

Este artículo es de .NET 1.1 pero aún debe aplicarse en las versiones actuales de Framework (aunque WCF es el método preferido).

Agrupación de objetos .NET

Thomas
fuente
+1 por mostrarme que la IInstanceProviderinterfaz existe ya que implementaré esto para mi solución. Siempre soy fanático de apilar mi código detrás de una interfaz proporcionada por Microsoft cuando proporcionan una definición adecuada.
Chris Marisic
4

Realmente me gusta la implementación de Aronaught, especialmente porque maneja la espera del recurso para estar disponible mediante el uso de un semáforo. Hay varias adiciones que me gustaría hacer:

  1. Cambiar sync.WaitOne()a sync.WaitOne(timeout)y exponer el tiempo de espera como parámetro de Acquire(int timeout)método. Esto también necesitaría manejar la condición cuando el hilo agota el tiempo de espera de que un objeto esté disponible.
  2. Agregue un Recycle(T item)método para manejar situaciones cuando un objeto necesita ser reciclado cuando ocurre una falla, por ejemplo.
Igor Pashchuk
fuente
3

Esta es otra implementación, con un número limitado de objetos en el grupo.

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.
fuente