qué objetos objeto método listas ejemplos dinamicos clases c# design-patterns pooling

método - Implementación del patrón de agrupación de objetos C#



¿ en c# qué es método? (9)

Agrupación de objetos en .NET Core

El núcleo de dotnet tiene una implementación de agrupación de objetos agregada a la biblioteca de clases base (BCL). Puede leer el problema original de GitHub here y ver el código para System.Buffers . Actualmente, ArrayPool es el único tipo disponible y se utiliza para agrupar arrays. Hay una buena publicación en el 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 this publicación de blog para obtener más información sobre cómo Newtonsoft.Json está haciendo esto.

Conjunto de objetos en Microsoft Roslyn C # Compiler

El nuevo compilador Microsoft Roslyn C # contiene el tipo ObjectPool , que se utiliza para agrupar objetos usados ​​frecuentemente que normalmente se reciclan y recogen basura con mucha frecuencia. 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: ¿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 : implementaciones no estrictamente separadas, sino envoltorios en torno a la implementación de SharedPools mostrados anteriormente específicamente para List y StringBuilder. 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 : usan 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 para objetos MemoryStream . Es un reemplazo System.IO.MemoryStream 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 declarar una vez y se mantendrá durante todo el proceso; este es el grupo. Está perfectamente bien usar múltiples piscinas si lo desea.

¿Alguien tiene un buen recurso para implementar una estrategia de grupo de objetos compartido 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).

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 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á presionando su servicio. 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.

Con suerte, ese fondo ofrece algún valor, pero para responder directamente a algunas de sus preguntas:

P: ¿Los objetos son caros de crear?
A: ningún objeto es un grupo de recursos limitados

P: ¿Serán adquiridos / lanzados con mucha frecuencia?
R: Sí, una vez más se pueden pensar en NHibernate ISessions donde 1 generalmente se adquiere y se libera para la duración de cada solicitud de página.

P: ¿Bastará con un simple "first-come-first-serve" o necesita algo más inteligente, es decir, que evite la inanición?
R: Una simple distribución tipo round robin sería suficiente, por inanición, supongo que te refieres a si no hay sesiones disponibles que las personas que llaman 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 grupo de objetos, por lo que originalmente dejé esta parte y planifiqué solo para adaptar el patrón para permitir el uso compartido de objetos en lugar de permitir que ocurra una situación de inanición.

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


Algo como esto podría adaptarse a tus 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); } } }



En la época en que 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.

Conjunto de objetos en WCF

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

Object Pooling .NET


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


Esta pregunta es un poco más complicada de lo que cabría esperar debido a varias incógnitas: el comportamiento del recurso agrupado, el tiempo de vida esperado / requerido de los objetos, el motivo real por el que se requiere el grupo, etc. Normalmente, los grupos tienen un propósito especial: hilo grupos, grupos de conexiones, etc. - porque es más fácil optimizar uno cuando 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 tratado de 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.

Un grupo de propósito general debería tener algunas "configuraciones" principales, que incluyen:

  • Estrategia de carga de recursos: ansioso o flojo;
  • Mecanismo de carga de recursos: cómo construir uno realmente;
  • Estrategia de acceso: mencionas "round robin" que no es tan sencillo como suena; 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 reclaman realmente 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 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 ya hemos terminado. 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 complicada, por lo que mi enfoque fue utilizar un enfoque basado en la 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: permitiremos que la clase Pool pública 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, usando una List<T> y un puntero de índice para aproximarme a un patrón de acceso circular. .

Todas las clases a continuación son clases internas del Pool<T> : esta fue una elección de estilo, pero como estas no están pensadas para 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 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; } } }

Pude haber escogido varios enfoques diferentes, pero la conclusión es que se debe acceder a los recursos en el mismo orden en que se crearon, lo que significa que debemos 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 grupo 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.

Agregue una enumeración y un método de fábrica y terminamos 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 para resolver es la estrategia de carga. Definí tres tipos:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Los dos primeros deberían ser autoexplicativos; 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).

Los métodos de carga realmente no son demasiado complicados, ahora que tenemos la abstracción item-store:

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 de size y count mencionados anteriormente se refieren al tamaño máximo del grupo y la cantidad total de recursos que posee el 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 controles de AcquireLazy para ver 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 (lo he probado en condiciones de subprocesos múltiples, pero obviamente no exhaustivamente).

Es posible que se pregunte por qué ninguno de estos métodos se molesta en verificar si la tienda alcanzó el tamaño máximo o no. Voy a llegar a eso en un momento.

Ahora para el grupo 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 asegurar que limitemos la cantidad total de recursos creados - resulta que .NET ya tiene una herramienta perfectamente buena para eso, se llama Semaphore y está diseñada específicamente para permitir un arreglo número de hilos que acceden a un recurso (en este caso, el "recurso" es el almacén interno de elementos). 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 carga ansiosa, utilizando el método PreloadItems ya mostrado anteriormente.

Como casi todo ha sido abstraído hasta ahora, los métodos de Acquire y Release realmente son 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 utilizando el Semaphore para controlar la concurrencia en lugar de verificar religiosamente el estado del almacén de artículos. Siempre que los artículos adquiridos se publiquen correctamente, no hay nada de qué preocuparse.

Por último, pero no menos importante, hay una 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 propiedad IsDisposed quedará claro en un momento. Todo lo que realmente hace el método principal de eliminación es deshacerse de los elementos agrupados reales si implementan IDisposable .

Ahora básicamente puedes usar esto como está, con un bloque try-finally , pero no me gusta esa sintaxis, porque si comienzas a pasar recursos agrupados entre clases y métodos, se volverá muy confuso. Es posible que la clase principal que usa 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 recurso desechable de Foo que implementa IFoo y tiene un código repetitivo para generar identidades únicas. Lo que hacemos es crear otro objeto especial, agrupado:

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 IFoo interno (podríamos hacer esto con una biblioteca Dynamic Proxy como Castle, pero no entraré en eso). También mantiene una referencia al Pool que lo crea, de modo que cuando Dispose este objeto, se libere automáticamente al pool. Excepto cuando el grupo ya ha sido eliminado, esto significa que estamos en modo de "limpieza" y en este caso, en realidad limpia el recurso interno .

Usando el enfoque anterior, tenemos que escribir un 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 IFoo (a diferencia del 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 su disfrute de copiar y pegar. También hay un breve programa de prueba que puede utilizar para jugar con diferentes modos de carga / acceso y condiciones de multiproceso, para asegurarse de que es seguro para la ejecución de subprocesos y no tiene errores.

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



Realmente me gusta 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 que me gustaría hacer:

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