Implementação do C # Object Pooling Pattern

Alguém tem um bom recurso sobre a implementação de uma estratégia de pool de objects compartilhados para um recurso limitado na veia de pool de conexões Sql? (ou seja, seria implementado totalmente que é thread-safe).

Para acompanhar a solicitação do @Aaronaught para esclarecimento, o uso do pool seria para solicitações de balanceamento de carga para um serviço externo. Para colocá-lo em um cenário que provavelmente seria mais fácil de entender imediatamente em oposição à minha situação direta. Eu tenho um object de session que funciona de forma semelhante ao object ISession do NHibernate. Que cada session única gerencia sua conexão com o database. Atualmente, tenho 1 object de session de longa execução e estou encontrando problemas em que o meu provedor de serviços está limitando meu uso dessa session individual.

Devido à falta de expectativa de que uma única session seria tratada como uma longa conta de serviço, eles aparentemente a tratam como um cliente que está marcanvasndo seu serviço. O que me leva à minha pergunta aqui, em vez de ter uma session individual, eu criaria um pool de sessões diferentes e dividiria as solicitações para o serviço nessas várias sessões, em vez de criar um único ponto focal como eu estava fazendo anteriormente.

Espero que essa experiência ofereça algum valor, mas que responda diretamente a algumas de suas perguntas:

P: Os objects são caros para criar?
R: Nenhum object é um pool de resources limitados

P: Eles serão adquiridos / liberados com muita frequência?
R: Sim, mais uma vez eles podem pensar em ISessões do NHibernate em que 1 é geralmente adquirido e liberado para a duração de cada solicitação de página.

P: Um simples first-come-first-serving é suficiente ou você precisa de algo mais inteligente, isto é, que previna a fome?
R: Uma simples distribuição do tipo round-robin seria suficiente, por fome eu suponho que você quer dizer que, se não houver sessões disponíveis, os chamadores ficarão bloqueados esperando pelos lançamentos. Isso não é realmente aplicável, pois as sessões podem ser compartilhadas por diferentes chamadores. Meu objective é distribuir o uso em várias sessões, em vez de uma única session.

Acredito que isso seja provavelmente uma divergência do uso normal de um pool de objects, e é por isso que eu originalmente deixei essa parte e planejei apenas adaptar o padrão para permitir o compartilhamento de objects, em vez de permitir que uma situação de fome ocorresse.

P: E sobre coisas como prioridades, carregamento lento versus carregamento ansioso, etc.?
R: Não há priorização envolvida, para simplificar apenas assumir que eu criaria o conjunto de objects disponíveis na criação do próprio pool.

Agrupamento de objects no núcleo do .NET

O núcleo de dotnet tem uma implementação de agrupamento de objects adicionada à biblioteca de classs base (BCL). Você pode ler o problema original do GitHub aqui e ver o código para System.Buffers . Atualmente, o ArrayPool é o único tipo disponível e é usado para agrupar arrays. Há um bom post aqui .

 namespace System.Buffers { public abstract class ArrayPool { public static ArrayPool Shared { get; internal set; } public static ArrayPool Create(int maxBufferSize = , int numberOfBuffers = ); public T[] Rent(int size); public T[] Enlarge(T[] buffer, int newSize, bool clearBuffer = false); public void Return(T[] buffer, bool clearBuffer = false); } } 

Um exemplo de seu uso pode ser visto no ASP.NET Core. Por estar no core dotnet BCL, o ASP.NET Core pode compartilhar seu pool de objects com outros objects, como o serializador JSON do Newtonsoft.Json. Você pode ler este post para mais informações sobre como o Newtonsoft.Json está fazendo isso.

Pool de objects no Microsoft Roslyn C # Compiler

O novo compilador Microsoft Roslyn C # contém o tipo ObjectPool , que é usado para agrupar objects usados ​​com frequência que normalmente seriam renovados e o lixo coletado com muita frequência. Isso reduz a quantidade e o tamanho das operações de garbage collection que precisam acontecer. Existem algumas sub-implementações diferentes, todas usando o ObjectPool (Veja: Por que existem tantas implementações do Object Pooling em Roslyn? ).

1 – SharedPools – Armazena um pool de 20 objects ou 100 se o BigDefault for usado.

 // Example 1 - In a using statement, so the object gets freed at the end. using (PooledObject pooledObject = SharedPools.Default>().GetPooledObject()) { // Do something with pooledObject.Object } // Example 2 - No using statement so you need to be sure no exceptions are not thrown. List list = SharedPools.Default>().AllocateAndClear(); // Do something with list SharedPools.Default>().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][4] object. This is probably the preferred option if you want fewer GC's. List list = SharedPools.Default>().AllocateAndClear(); try { // Do something with list } finally { SharedPools.Default>().Free(list); } 

2 – ListPool e StringBuilderPool – Implementações não estritamente separadas, mas wrappers em torno da implementação de SharedPools mostrada acima especificamente para List e StringBuilder. Então, isso reutiliza o conjunto de objects armazenados em 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 – Eles usam o ObjectPool diretamente e possuem um pool de objects totalmente separado. Armazena um conjunto de 128 objects.

 // Example 1 PooledHashSet hashSet = PooledHashSet.GetInstance() // Do something with hashSet. hashSet.Free(); // Example 2 - Safer version of Example 1. PooledHashSet hashSet = PooledHashSet.GetInstance() try { // Do something with hashSet. } finally { hashSet.Free(); } 

Microsoft.IO.RecyclableMemoryStream

Esta biblioteca fornece pool para objects MemoryStream . É um substituto para o System.IO.MemoryStream . Tem exatamente a mesma semântica. Foi projetado por engenheiros do Bing. Leia a postagem do blog aqui ou veja o código no 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); } 

Observe que o RecyclableMemoryStreamManager deve ser declarado uma vez e viverá durante todo o processo – esse é o pool. Não há problema em usar vários pools, se desejar.

Essa questão é um pouco mais complicada do que se poderia esperar devido a várias incógnitas: o comportamento do recurso sendo agrupado, o tempo de vida esperado / necessário dos objects, o motivo real do qual o pool é necessário etc. pools, pools de conexão, etc. – porque é mais fácil otimizar um quando você sabe exatamente o que o recurso faz e, mais importante, tem controle sobre como esse recurso é implementado.

Como não é tão simples, o que eu tentei fazer foi oferecer uma abordagem bastante flexível que você possa experimentar e ver o que funciona melhor. Pedimos desculpas antecipadamente pelo longo período, mas há muito o que fazer quando se trata de implementar um pool de resources decente para fins gerais. e estou realmente apenas arranhando a superfície.

Um pool de propósito geral teria que ter algumas “configurações” principais, incluindo:

  • Estratégia de carregamento de resources – ansiosa ou preguiçosa;
  • Mecanismo de carregamento de resources – como realmente construir um;
  • Estratégia de access – você menciona “round robin”, que não é tão simples quanto parece; Essa implementação pode usar um buffer circular que é semelhante , mas não perfeito, porque o pool não tem controle sobre quando os resources são realmente recuperados. Outras opções são FIFO e LIFO; O FIFO terá mais de um padrão de access random, mas o LIFO facilita significativamente a implementação de uma estratégia de liberação de uso mais recente (que você disse estar fora do escopo, mas ainda vale a pena mencionar).

Para o mecanismo de carregamento de resources, o .NET já nos fornece uma abstração limpa – delegates.

 private Func, T> factory; 

Passe isto através do construtor da piscina e estamos quase terminando com isso. Usar um tipo genérico com uma restrição new() também funciona, mas isso é mais flexível.


Dos outros dois parâmetros, a estratégia de access é a mais complicada, então minha abordagem foi usar uma abordagem baseada em inheritance (interface):

 public class Pool : IDisposable { // Other code - we'll come back to this interface IItemStore { T Fetch(); void Store(T item); int Count { get; } } } 

O conceito aqui é simples – deixaremos a class Pool pública lidar com os problemas comuns, como thread-safety, mas usar um “armazenamento de itens” diferente para cada padrão de access. O LIFO é facilmente representado por uma pilha, FIFO é uma fila e usei uma implementação de buffer circular não muito otimizada, mas provavelmente adequada, usando um List e um ponteiro de índice para aproximar um padrão de access round-robin .

Todas as classs abaixo são classs internas do Pool – essa foi uma escolha de estilo, mas como elas não devem ser usadas fora do Pool , isso faz mais sentido.

  class QueueStore : Queue, IItemStore { public QueueStore(int capacity) : base(capacity) { } public T Fetch() { return Dequeue(); } public void Store(T item) { Enqueue(item); } } class StackStore : Stack, IItemStore { public StackStore(int capacity) : base(capacity) { } public T Fetch() { return Pop(); } public void Store(T item) { Push(item); } } 

Estes são os mais óbvios – pilha e fila. Eu não acho que eles realmente justifiquem muita explicação. O buffer circular é um pouco mais complicado:

  class CircularStore : IItemStore { private List slots; private int freeSlotCount; private int position = -1; public CircularStore(int capacity) { slots = new List(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; } } } 

Eu poderia ter escolhido várias abordagens diferentes, mas a conclusão é que os resources devem ser acessados ​​na mesma ordem em que foram criados, o que significa que precisamos manter referências a eles, mas marcá-los como “em uso” (ou não). ). Na pior das hipóteses, apenas um slot está disponível e é necessária uma iteração completa do buffer para cada busca. Isso é ruim se você tem centenas de resources agrupados e os está adquirindo e liberando várias vezes por segundo; não é realmente um problema para um conjunto de 5-10 itens e, no caso típico , em que os resources são usados ​​levemente, ele precisa avançar apenas um ou dois slots.

Lembre-se, essas classs são classs internas privadas – é por isso que elas não precisam de muita verificação de erros, o próprio pool restringe o access a elas.

Acrescente uma enumeração e um método de fábrica e concluímos 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); } } 

O próximo problema a resolver é a estratégia de carregamento. Eu defini três tipos:

 public enum LoadingMode { Eager, Lazy, LazyExpanding }; 

Os dois primeiros devem ser auto-explicativos; o terceiro é uma espécie de híbrido, carrega com preguiça os resources, mas na verdade não começa a reutilizar nenhum recurso até que o pool esteja cheio. Isso seria um bom compromisso se você quiser que o pool esteja cheio (o que parece que você faz), mas deseja adiar a despesa de realmente criá-los até o primeiro access (ou seja, melhorar os tempos de boot).

Os methods de carregamento não são muito complicados, agora que temos a abstração de armazenamento de itens:

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

Os campos de size e count acima referem-se ao tamanho máximo do conjunto e ao número total de resources pertencentes ao conjunto (mas não necessariamente disponíveis ), respectivamente. AcquireEager é o mais simples, ele assume que um item já está na loja - esses itens seriam pré-carregados na construção, ou seja, no método PreloadItems mostrado por último.

AcquireLazy verifica se há itens gratuitos no pool e, caso contrário, cria um novo. AcquireLazyExpanding criará um novo recurso, desde que o pool ainda não tenha atingido o tamanho desejado. Eu tentei otimizar isso para minimizar o bloqueio, e espero não ter cometido nenhum erro (testei isso em condições multi-threaded, mas obviamente não exaustivamente).

Você pode estar se perguntando por que nenhum desses methods incomoda verificar se a loja atingiu ou não o tamanho máximo. Eu vou chegar a isso em um momento.


Agora para a piscina em si. Aqui está o conjunto completo de dados privados, alguns dos quais já foram mostrados:

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

Respondendo a pergunta que eu encarei no último parágrafo - como garantir que nós limitamos o número total de resources criados - acontece que o .NET já tem uma ferramenta perfeitamente boa para isso, é chamado Semaphore e é projetado especificamente para permitir uma correção número de accesss de encadeamentos a um recurso (neste caso, o "recurso" é o armazenamento interno de itens). Como não estamos implementando uma fila completa de produtor / consumidor, isso é perfeitamente adequado às nossas necessidades.

O construtor é assim:

  public Pool(int size, Func, 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(); } } 

Não deve haver surpresas aqui. A única coisa a notar é o revestimento especial para carregamento PreloadItems , usando o método PreloadItems já mostrado anteriormente.

Como quase tudo já foi totalmente eliminado, os methods reais de Acquire e Release são realmente diretos:

  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 explicamos anteriormente, estamos usando o Semaphore para controlar a simultaneidade, em vez de verificar religiosamente o status do armazenamento de itens. Desde que os itens adquiridos sejam liberados corretamente, não há nada com que se preocupar.

Por último, mas não menos importante, há limpeza:

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

O propósito dessa propriedade IsDisposed ficará claro em um momento. Tudo o que o método Dispose principal realmente faz é descartar os itens reais em pool se eles implementarem IDisposable .


Agora você pode usar isso basicamente como está, com um bloco try-finally , mas não gosto dessa syntax, porque se você começar a passar por resources em pool entre classs e methods, isso ficará muito confuso. É possível que a class principal que usa um recurso nem tenha uma referência ao pool. Isso realmente se torna bastante confuso, então uma abordagem melhor é criar um object em pool "inteligente".

Digamos que começamos com a seguinte interface / class simples:

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

Aqui está o nosso recurso descartável de Foo que implementa IFoo e possui algum código clichê para gerar identidades únicas. O que fazemos é criar outro object especial em pool:

 public class PooledFoo : IFoo { private Foo internalFoo; private Pool pool; public PooledFoo(Pool 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(); } } 

Isso apenas faz o proxies de todos os methods "reais" para o seu IFoo interno (nós poderíamos fazer isso com uma biblioteca de Dynamic Proxy como Castle, mas eu não vou entrar nisso). Ele também mantém uma referência ao Pool que o cria, de forma que quando Dispose esse object, ele automaticamente se libera de volta ao pool. Exceto quando o pool já foi descartado - isso significa que estamos no modo de "limpeza" e, nesse caso, ele realmente limpa o recurso interno .


Usando a abordagem acima, conseguimos escrever código como este:

 // Create the pool early Pool pool = new Pool(PoolSize, p => new PooledFoo(p), LoadingMode.Lazy, AccessMode.Circular); // Sometime later on... using (IFoo foo = pool.Acquire()) { foo.Test(); } 

Isso é uma coisa muito boa para ser capaz de fazer. Isso significa que o código que usa o IFoo (em oposição ao código que o cria) não precisa estar ciente do pool. Você pode até mesmo injetar objects IFoo usando sua biblioteca DI favorita e o Pool como provedor / fábrica.


Eu coloquei o código completo no PasteBin para o seu prazer de copiar e colar. Há também um programa de teste curto que você pode usar para brincar com diferentes modos de carregamento / access e condições multithread, para se certificar de que é thread-safe e não de bugs.

Deixe-me saber se você tem dúvidas ou preocupações sobre isso.

Algo assim pode atender às suas necessidades.

 ///  /// Represents a pool of objects with a size limit. ///  /// The type of object in the pool. public sealed class ObjectPool : IDisposable where T : new() { private readonly int size; private readonly object locker; private readonly Queue queue; private int count; ///  /// Initializes a new instance of the ObjectPool class. ///  /// The size of the object pool. 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(); } ///  /// Retrieves an item from the pool. ///  /// The item retrieved from the pool. public T Get() { lock (locker) { if (queue.Count > 0) { return queue.Dequeue(); } count++; return new T(); } } ///  /// Places an item in the pool. ///  /// The item to place to the pool. public void Put(T item) { lock (locker) { if (count < size) { queue.Enqueue(item); } else { using (item as IDisposable) { count--; } } } } ///  /// Disposes of items in the pool that implement IDisposable. ///  public void Dispose() { lock (locker) { count = 0; while (queue.Count > 0) { using (queue.Dequeue() as IDisposable) { } } } } } 

Exemplo de uso

 public class ThisObject { private readonly ObjectPool pool = new ObjectPool(100); public void ThisMethod() { var that = pool.Get(); try { // Use that .... } finally { pool.Put(that); } } } 

Exemplo da MSDN: Como: criar um pool de objects usando um ConcurrentBag

Voltar no dia Microsoft forneceu uma estrutura por meio do Microsoft Transaction Server (MTS) e posterior COM + para fazer o pool de object para objects COM. Essa funcionalidade foi transportada para System.EnterpriseServices no .NET Framework e agora no Windows Communication Foundation.

Pool de objects no WCF

Este artigo é do .NET 1.1, mas ainda deve ser aplicado nas versões atuais do Framework (embora o WCF seja o método preferido).

Pool de Objetos .NET

Eu realmente gosto da implementação do Aronaught – especialmente porque ele lida com a espera no recurso para se tornar disponível através do uso de um semáforo. Há várias adições que gostaria de fazer:

  1. Altere sync.WaitOne() para sync.WaitOne(timeout) e exponha o tempo limite como um parâmetro no método Acquire(int timeout) . Isso também exigiria o tratamento da condição quando o encadeamento expirar, esperando que um object fique disponível.
  2. Adicione o método Recycle(T item) para lidar com situações em que um object precisa ser reciclado quando ocorre uma falha, por exemplo.

Esta é outra implementação, com número limitado de objects no pool.

 public class ObjectPool where T : class { private readonly int maxSize; private Func constructor; private int currentSize; private Queue pool; private AutoResetEvent poolReleasedEvent; public ObjectPool(int maxSize, Func constructor) { this.maxSize = maxSize; this.constructor = constructor; this.currentSize = 0; this.pool = new Queue(); 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(); } } } 

Orientado a Java, este artigo expõe o padrão de conjunto connectionImpl e o padrão de conjunto de object abstraído e pode ser uma boa primeira abordagem: http://www.developer.com/design/article.php/626171/Pattern-Summaries-Object-Pool. htm

Padrão de pool de objects:

padronizar

Uma extensão do msdn é como criar um pool de objects usando um ConcurrentBag.

https://github.com/chivandikwa/ObjectPool