2010-03-24 12 views
145

¿Alguien tiene un buen recurso para implementar una estrategia de grupo de objetos compartidos para un recurso limitado en la línea de agrupación de conexiones Sql? (es decir, se implementaría completamente que es seguro para subprocesos).Implementación del patrón de agrupamiento de objetos C#

Para realizar un seguimiento en relación con @Aaronaught solicitud de aclaración, 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 comparación con mi situtation directa. Tengo un objeto de sesión que funciona de manera similar al objeto ISession de NHibernate. Que cada sesión única maneje su conexión a la base de datos. Actualmente tengo 1 objeto de sesión larga y estoy encontrando problemas en los que mi proveedor de servicios limita mi uso de esta sesión individual.

Debido a su falta de expectativa 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á trabajando duro. Lo que me lleva a mi pregunta aquí, en lugar de tener 1 sesión individual, crearía un grupo de sesiones diferentes y dividiría las solicitudes entre las múltiples sesiones en lugar de crear un solo punto focal como lo hacía anteriormente.

de esperar que el fondo ofrece algún valor, pero para responder directamente a algunas de sus preguntas:

Q: son los objetos caros para crear?
A: No hay objetos son un conjunto de recursos limitados

Q: Van a ser adquiridos/liberados con mucha frecuencia?
A: Sí, una vez más se pueden pensar en NHibernate ISessions donde 1 suele adquirirse y liberarse durante la duración de cada solicitud de página.

Q: ¿Bastará con un primer orden de llegada o necesita algo más inteligente, es decir, que evite la inanición?
A: Una simple distribución de tipo round robin sería suficiente, por inanición supongo que quiere decir que no hay sesiones disponibles que los llamantes se bloqueen a la espera de lanzamientos. Esto no es realmente aplicable ya que las diferentes personas pueden compartir las sesiones. 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 conjunto de objetos por lo que originalmente dejé esta parte y planeé solo adaptar el patrón para permitir compartir objetos en lugar de permitir que ocurra una situación de inanición .

Q: ¿Qué pasa con cosas como las prioridades, la carga lenta o impaciente, etc.?
A: No hay ninguna priorización involucrada, por simplicidad simplemente asuma que crearía el grupo de objetos disponibles en la creación del grupo mismo.

+1

¿Puede decirnos algo acerca de sus requisitos?No todos los grupos se crean iguales. ¿Los objetos son caros de crear? ¿Serán adquiridos/lanzados con mucha frecuencia? ¿Bastará con un simple primero que llegue primero o necesita algo más inteligente, es decir, que evite la inanición? ¿Qué pasa con cosas como las prioridades, la carga lenta o impaciente, etc.? Cualquier cosa que pueda agregar nos ayudaría (o al menos a mí) a encontrar una respuesta más completa. – Aaronaught

+0

Chris - solo mirando sus párrafos segundo y tercero, y preguntándome si estas sesiones realmente deberían mantenerse vivas indefinidamente. Parece que eso es lo que no le gusta a su proveedor de servicios (sesiones prolongadas), por lo que podría estar buscando una implementación de grupo que active las sesiones nuevas según sea necesario y las apague cuando no se usen (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

+0

No estoy seguro de si necesito una solución robusta o no, ya que mi solución es meramente hipotética. Es posible que mi proveedor de servicios me esté mintiendo y que su servicio ya se haya vendido y simplemente haya encontrado una excusa para culpar al usuario. –

Respuesta

34

Agrupación de objetos en .NET Core

El dotnet core tiene una implementación de la agrupación de objetos añade a la biblioteca de clases base (BCL). Puede leer el número original de GitHub here y ver el código para System.Buffers. Actualmente, el ArrayPool es el único tipo disponible y se utiliza para agrupar arrays. Hay una buena entrada de blog here.

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 BPC de núcleo de dotnet, ASP.NET Core puede compartir su grupo de objetos con otros objetos como el serializador JSON de Newtonsoft.Json. Puede leer la publicación del blog this para obtener más información sobre cómo Newtonsoft.Json está haciendo esto.

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

El nuevo Microsoft Roslyn compilador de C# contiene el tipo ObjectPool, que se utiliza para agrupar objetos de uso frecuente, que normalmente te new'ed y basuras recogidas muy a menudo. Esto reduce la cantidad y el tamaño de las operaciones de recolección de basura que tienen que suceder. Hay algunas sub-implementaciones diferentes que usan ObjectPool (Ver: Why are there so many implementations of Object Pooling in Roslyn?).

1 - SharedPools - Almacena un conjunto de 20 objetos o 100 si se utiliza 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 - implementaciones No es estrictamente separadas, sino envolturas alrededor de la implementación SharedPools se muestran arriba específicamente para List y StringBuilder de. 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 - Estos ObjectPool utilizar directamente y tienen una piscina totalmente separada de los objetos. 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 la puesta en común para MemoryStream objetos. Es un reemplazo directo para System.IO.MemoryStream. Tiene exactamente la misma semántica. Fue diseñado por ingenieros de Bing. Lea la publicación del blog here 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 RecyclableMemoryStreamManager debe declararse una vez y vivirá durante todo el proceso de esto es la piscina. Está perfectamente bien usar múltiples piscinas si lo desea.

+1

Esta es una gran respuesta. Después de que C# 6 y VS2015 sean RTM, probablemente lo haga la respuesta aceptada, ya que es claramente lo mejor de todo si está tan afinado como lo usa Rosyln. –

+0

Estoy de acuerdo, pero ¿qué implementación usaría? Roslyn contiene tres. Ver el enlace a mi pregunta en la respuesta. –

+1

Parece que cada uno tiene un propósito claramente definido, mucho mejor que * solo * la elección de un zapato de talla única para todos los zapatos. –

4

De vuelta en el día Microsoft proporcionó un marco a través de Microsoft Transaction Server (MTS) y más tarde COM + para agrupar objetos para objetos COM. Esa funcionalidad se transfirió a System.EnterpriseServices en .NET Framework y ahora en Windows Communication Foundation.

Object Pooling in WCF

Este artículo es de .NET 1.1, pero todavía se debe aplicar en las versiones actuales del Marco (aunque WCF es el método preferido).

Object Pooling .NET

+0

+1 por mostrarme que existe la interfaz 'IInstanceProvider' ya que implementaré esto para mi solución. Siempre me gusta apilar mi código detrás de una interfaz proporcionada por Microsoft cuando proporcionan una definición adecuada. –

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

Raspe ese comentario anterior. Creo que me pareció extraño porque este grupo no parece tener ningún umbral, y tal vez no lo necesite, dependerá de los requisitos. – Aaronaught

+1

@Aaronaught - ¿Es realmente tan extraño? Quería crear un grupo liviano que ofreciera solo 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 que sea List/HashTable, etc. y cambiando el contador para que se vuelque. Pregunta aleatoria ¿cómo se maneja la gestión del objeto de la piscina en sí? ¿Simplemente lo pegas en un contenedor de IOC que lo define como singleton allí? –

288

Esta pregunta es un poco más difícil de lo que cabría esperar debido a varias incógnitas: El comportamiento del recurso que se agruparon, el tiempo de vida esperado/requerido de los objetos, las razón real por la que se requiere el grupo, etc. Normalmente, los grupos son de propósito especial: grupos de subprocesos, grupos de conexiones, etc., porque es más fácil optimizar uno cuando se sabe exactamente lo que hace el recurso y más importante, controlar cómo ese recurso es implementar ed.

Dado que 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 publicación larga, pero hay mucho terreno por recorrer cuando se trata de implementar un conjunto de recursos de propósito general decente. y realmente solo estoy rascando la superficie.

Una piscina de uso general tendría que tener algunos "ajustes" principales, incluyendo: estrategia de carga

  • de Recursos - ansiosos o perezoso;
  • Recurso cargando mecanismo - cómo construir realmente uno;
  • Estrategia de acceso: mencionas "round robin" que no es tan sencillo como suena; esta implementación puede usar un buffer circular que es similar, pero no es perfecto, porque el grupo no tiene control sobre cuando los recursos son realmente recuperados. 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 utilizada menos recientemente (que usted dijo que estaba fuera del alcance, pero aún así 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 agrupación y hemos terminado con eso. Usar un tipo genérico con una restricción new() también funciona, pero esto es más flexible.


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

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 - Permitirá que la clase pública Pool maneje los problemas comunes como la seguridad de subprocesos, pero use 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 memoria intermedia circular no muy optimizada pero probablemente adecuada con un List<T> y un puntero de índice para aproximarme a un patrón de acceso circular.

Todas las clases siguientes son clases internas del Pool<T> - esta fue una elección de estilo, pero como estas no están destinadas a ser utilizadas 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); 
     } 
    } 

Estas son las obvias: apilar y hacer cola. No creo que realmente justifiquen mucha explicación. El buffer 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; } 
     } 
    } 

que podría haber elegido un número de diferentes enfoques, pero la conclusión es que deben tener acceso a los recursos en el mismo orden en que fueron creados, lo que significa que tenemos 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 está adquiriendo y liberándolos varias veces por segundo; realmente no es un problema para un conjunto de 5-10 elementos, y en el caso típico, donde los recursos se utilizan a la ligera, 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 comprobación de errores, el grupo en sí restringe el acceso a ellos.

El tiro en la 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 Estrategia de carga. He definido tres tipos:

public enum LoadingMode { Eager, Lazy, LazyExpanding }; 

Las dos primeras deben ser autoexplicativas; el tercero es una especie de híbrido, carga los recursos de manera lenta pero en realidad no comienza a reutilizar ningún recurso hasta que el grupo esté lleno. Esta sería una buena solución si desea que el grupo esté lleno (lo cual suena como lo hace) pero desea diferir el gasto de crearlos realmente hasta el primer acceso (es decir, para mejorar los tiempos de inicio).

La carga métodos realmente no son demasiado complicados, ahora que tenemos el elemento de la tienda abstracción:

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

El size y count campos anteriormente se refieren al tamaño máximo de la piscina y el número total de recursos propiedad del grupo (pero no necesariamente disponible), respectivamente. AcquireEager es el más simple, supone que un artículo ya está en la tienda; estos elementos se precargarán en la construcción, es decir, en el método PreloadItems que se muestra al final.

AcquireLazy comprueba si hay elementos gratuitos en el grupo, y si no, crea uno nuevo. AcquireLazyExpanding creará un nuevo recurso siempre que el grupo aún no haya alcanzado su tamaño objetivo. Intenté optimizar esto para minimizar el bloqueo, y espero no haber cometido ningún error (I ha probado bajo condiciones de subprocesos múltiples, pero obviamente no exhaustivamente).

Quizás se pregunte por qué ninguno de estos métodos se molesta en comprobar si la tienda ha alcanzado o no el tamaño máximo. Voy a llegar a eso en un momento.


Ahora para la piscina en sí. Éste es el conjunto completo de datos privados, algunos de los cuales ya se ha demostrado:

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 asegurar que limitamos 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 que un número fijo de hilos acceda a un recurso (en este caso, el "recurso" es el almacén interno de artículos). Como no estamos implementando una cola completa de productores/consumidores, esto es perfectamente adecuado para nuestras necesidades.

El constructor tiene el siguiente aspecto:

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

debe haber sorpresas aquí. Lo único a tener en cuenta es la carcasa especial para la carga ansiosa, utilizando el método PreloadItems ya mostrado anteriormente.

Dado que casi todo ha sido resumidas limpiamente por ahora, los actuales Acquire y Release métodos son realmente muy sencillo:

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 ha explicado anteriormente, estamos usando la Semaphore para controlar la concurrencia en lugar de religiosamente el control de la estado de la tienda de artículos. Siempre que los artículos adquiridos se publiquen correctamente, no hay nada de qué preocuparse.

Por último, pero no menos importante, es la 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 ese IsDisposed propiedad se aclarará en un momento. Todo el método principal Dispose realmente es deshacerse de los elementos agrupados reales si implementan IDisposable.


Ahora básicamente se puede utilizar esto como se encuentra, y un bloque try-finally, pero no soy aficionado a esa sintaxis, porque si comienza a pasar recursos en torno agrupados entre las clases y los métodos entonces va a ser muy confuso. Es posible que la clase principal que utiliza un recurso ni siquiera tenga como referencia al grupo. Realmente se vuelve bastante desordenado, por lo que un mejor enfoque es crear un objeto agrupado "inteligente".

Digamos que empezamos con la siguiente sencilla interfaz/clase:

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á nuestra pretender desechable Foo recursos que implementa IFoo y tiene algo de código reutilizable para generar identidades únicas. Lo que hacemos es crear otra, objeto especial agrupada:

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 se utiliza proxies todos los métodos "reales" en su interior IFoo (podríamos hacer esto con una biblioteca de proxy dinámico como el Castillo, pero ganó' entrar en eso). También mantiene una referencia al Pool que lo crea, de modo que cuando obtenemos este objeto, Dispose se libera automáticamente al grupo. Excepto cuando el grupo ya se ha eliminado, esto significa que estamos en modo "limpieza" y en este caso realmente limpia el recurso interno.


Utilizando el enfoque anterior, se llega a 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(); 
} 

Esta es una muy buena cosaa ser capaz de hacerlo. Significa que el código que usaIFoo (a diferencia del código que lo crea) en realidad no necesita conocer el grupo. Puede incluso inyectar objetosIFoo usando su biblioteca DI favorita y Pool<T> como proveedor/fábrica.


He puesto el complete code on PasteBin para su disfrute de copiar y pegar. También hay un corto test program que puede usar para jugar con diferentes modos de carga/acceso y condiciones multiproceso, para asegurarse de que es seguro para la ejecución de subprocesos y no tiene errores.

Deseo saber si tiene alguna pregunta o inquietud acerca de esto.

+59

Una de las respuestas más completas, útiles e interesantes que he leído en SO. –

+0

No podría estar más de acuerdo con @Josh sobre esta respuesta, especialmente para la parte de PooledFoo, ya que la liberación de los objetos siempre parecía manejarse de manera muy permeable y había imaginado que sería más lógico que fuera posible utilizar el Usando un constructo como el que mostraste, simplemente no me había sentado y traté de construirlo, ya que tu 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 bastante, ya que puedo compartir las instancias entre subprocesos y no es necesario volver a lanzarlas al grupo. –

+0

Sin embargo, si el enfoque simple no funciona primero, tengo algunas ideas en mi cabeza acerca de cómo podría manejar inteligentemente la versión para mi caso. Creo que más específicamente establecería el lanzamiento para poder determinar que la sesión misma falló y deshacerme de ella y reemplazar una nueva en el grupo. Independientemente de que esta publicación en este punto sea prácticamente la guía definitiva sobre la agrupación de objetos en C# 3.0, estoy deseoso de ver si alguien más tiene más comentarios al respecto. –

4

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

  1. Cambio sync.WaitOne() a sync.WaitOne(timeout) y exponer el tiempo de espera como parámetro en Acquire(int timeout) método. Esto también necesitaría manejar la condición cuando el hilo excede el tiempo de espera en que un objeto esté disponible.
  2. Método de Agregar Recycle(T item) para manejar situaciones cuando un objeto necesita ser reciclado cuando ocurre una falla, por ejemplo.
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(); 
     } 
    } 
} 
3
+0

Gracias por ese enlace. No existe un 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. –

+0

Agradable y simple –

Cuestiones relacionadas