Implementação de padrão de pool de objetos em C #

165

Alguém tem um bom recurso para implementar uma estratégia de pool de objetos compartilhados para um recurso limitado na veia do pool de conexões SQL? (ou seja, seria totalmente implementado que é seguro para threads).

Para acompanhar a solicitação de esclarecimento do @Aaronaught, 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 objeto de sessão que funciona de maneira semelhante ao ISessionobjeto do NHibernate. Que cada sessão exclusiva gerencia sua conexão com o banco de dados. Atualmente, tenho 1 objeto de sessão de execução longa e estou encontrando problemas em que meu provedor de serviços está limitando a taxa de uso dessa sessão individual.

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

Espero que esse plano de fundo ofereça algum valor, mas responda diretamente a algumas de suas perguntas:

P: Os objetos são caros para criar?
R: Nenhum objeto é um conjunto de recursos limitados

P: Eles serão adquiridos / liberados com muita frequência?
R: Sim, mais uma vez, eles podem ser pensados ​​nas NHibernate ISessions, onde 1 é normalmente adquirido e liberado durante toda a solicitação de uma única página.

P: Um simples primeiro a chegar, primeiro a servir é suficiente ou você precisa de algo mais inteligente, ou seja, que evite a fome?
R: Uma distribuição simples do tipo round robin seria suficiente, por inanição, suponho que você queira dizer se não houver sessões disponíveis em que os chamadores sejam bloqueados aguardando liberações. Isso não é realmente aplicável, pois as sessões podem ser compartilhadas por diferentes chamadores. Meu objetivo é distribuir o uso por várias sessões, em vez de uma única sessão.

Acredito que isso seja provavelmente uma divergência em relação ao uso normal de um pool de objetos, motivo pelo qual deixei essa parte de fora e planejei apenas adaptar o padrão para permitir o compartilhamento de objetos, em vez de permitir que uma situação de inanição ocorra.

P: E quanto a coisas como prioridades, carregamento lento ou lento, etc.?
R: Não há priorização envolvida, por uma questão de simplicidade, basta supor que eu criaria o pool de objetos disponíveis na criação do próprio pool.

Chris Marisic
fonte
1
Você pode nos contar um pouco sobre suas necessidades? Nem todos os conjuntos são criados iguais. Os objetos são caros para criar? Eles serão adquiridos / liberados com muita frequência? Um simples primeiro a chegar, primeiro a servir é suficiente ou você precisa de algo mais inteligente, ou seja, que evite a fome? E quanto a coisas como prioridades, carregamento preguiçoso x ansioso etc.? Qualquer coisa que você possa adicionar nos ajudaria (ou pelo menos a mim) a encontrar uma resposta mais completa.
Aaronaught
Chris - apenas olhando para o segundo e terceiro parágrafos e se perguntando se essas sessões realmente devem ser mantidas vivas indefinidamente? Parece que é disso que o seu provedor de serviços não gosta (sessões de longa execução) e, portanto, você pode estar procurando uma implementação de pool que acelere novas sessões conforme necessário e as encerre quando não estiver em uso (após um período especificado) . Isso pode ser feito, mas é um pouco mais complicado, então gostaria de confirmar.
Aaronaught
Não tenho certeza se preciso de uma solução robusta ou ainda não, pois minha solução é meramente hipotética. É possível que meu provedor de serviços esteja apenas mentindo para mim e que o serviço tenha sido vendido em excesso e tenha encontrado apenas uma desculpa para culpar o usuário.
quer tocar hoje
1
Eu acho que o TPL DataFlow BufferBlock faz a maior parte do que você precisa.
spender
1
O pool em ambientes encadeados é um problema recorrente, resolvido por padrões de design, como Pool de Recursos e Cache de Recursos. Confira Arquitetura de software orientada a padrões, volume 3: padrões para gerenciamento de recursos para obter mais informações.
Fuhrmanator

Respostas:

59

Pool de objetos no .NET Core

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

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

Um exemplo de seu uso pode ser visto no ASP.NET Core. Por estar no BCL do pontonet core, o ASP.NET Core pode compartilhar seu pool de objetos com outros objetos, como o serializador JSON do Newtonsoft.Json. Você pode ler esta postagem no blog para obter mais informações sobre como a Newtonsoft.Json está fazendo isso.

Pool de objetos no Microsoft Roslyn C # Compiler

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

1 - SharedPools - Armazena um pool de 20 objetos ou 100 se o BigDefault for usado.

// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
    // Do something with pooledObject.Object
}

// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);

// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][4] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
    // Do something with list
}
finally
{
    SharedPools.Default<List<Foo>>().Free(list);
}

2 - ListPool e StringBuilderPool - Não são implementações estritamente separadas, mas envolvem a implementação do SharedPools mostrada acima, especificamente para List e StringBuilder. Portanto, isso reutiliza o pool de objetos armazenados no 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 objetos totalmente separado. Armazena um pool 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 fornece pool para MemoryStreamobjetos. É um substituto para o drop-in 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 RecyclableMemoryStreamManagerdeve ser declarado uma vez e permanecerá durante todo o processo - este é o pool. É perfeitamente bom usar várias piscinas, se desejar.

Muhammad Rehan Saeed
fonte
2
Esta é uma ótima resposta. Depois que o C # 6 e o ​​VS2015 forem RTM, provavelmente tornarei essa a resposta aceita, já que é claramente a melhor de todas, se estiver sintonizado e usado pela própria Rosyln.
precisa saber é o seguinte
Concordo, mas que implementação você usaria? Roslyn contém três. Veja o link da minha pergunta na resposta.
Muhammad Rehan Saeed
1
Parece que cada um tem finalidades claramente definidas, muito melhor do que apenas a escolha de um tamanho aberto e aberto para todos os sapatos.
precisa saber é o seguinte
1
@MuhammadRehanSaeed grande adição com o ArrayPool
Chris Marisic
1
Vendo o RecyclableMemoryStreamque é uma adição incrível para otimizações de desempenho ultra alto.
22618 Chris Marisic
315

Essa pergunta é um pouco mais complicada do que se poderia esperar devido a várias incógnitas: o comportamento do recurso que está sendo agrupado, a vida útil esperada / necessária dos objetos, o motivo real pelo qual o pool é necessário etc. Geralmente, os pools são para fins especiais - thread 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 tentei fazer é oferecer uma abordagem bastante flexível com a qual você possa experimentar e ver o que funciona melhor. Pedimos desculpas antecipadamente pelo longo cargo, mas há muito a ser abordado quando se trata de implementar um conjunto decente de recursos de uso geral. e estou realmente apenas arranhando a superfície.

Um pool de uso geral teria que ter algumas "configurações" principais, incluindo:

  • Estratégia de carregamento de recursos - ansiosa ou preguiçosa;
  • Mecanismo de carregamento de recursos - como realmente construir um;
  • Estratégia de acesso - você menciona "round robin", que não é tão direto 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 recursos são realmente recuperados. Outras opções são FIFO e LIFO; O FIFO terá mais de um padrão de acesso aleatório, mas o LIFO facilita significativamente a implementação de uma estratégia de liberação Menos usados ​​recentemente (que você disse que estava fora do escopo, mas ainda vale a pena mencionar).

Para o mecanismo de carregamento de recursos, o .NET já nos fornece uma abstração limpa - delegados.

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

Passe isso pelo construtor do pool e estamos prontos para isso. O uso de um tipo genérico com new()restrição também funciona, mas isso é mais flexível.


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

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

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

O conceito aqui é simples - vamos deixar a Poolclasse pública lidar com problemas comuns, como segurança de threads, mas usar um "armazenamento de itens" diferente para cada padrão de acesso. O LIFO é facilmente representado por uma pilha, o FIFO é uma fila e eu usei uma implementação de buffer circular não muito otimizada, mas provavelmente adequada usando um List<T>ponteiro e index para aproximar um padrão de acesso round-robin.

Todas as classes abaixo são classes internas do Pool<T>- essa foi uma escolha de estilo, mas como elas realmente não devem ser usadas fora Pooldela, faz mais 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);
        }
    }

Estes são os ó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<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; }
        }
    }

Eu poderia ter escolhido uma série de abordagens diferentes, mas a linha inferior é que os recursos devem ser acessados ​​na mesma ordem em que foram criados, o que significa que precisamos manter referências a elas, mas marcá-las como "em uso" (ou não ) No pior cenário, apenas um slot está disponível e é necessária uma iteração completa do buffer para cada busca. Isso é ruim se você tiver centenas de recursos reunidos e estiver adquirindo e liberando-os várias vezes por segundo; não é realmente um problema para um conjunto de 5 a 10 itens e, no caso típico , onde os recursos são pouco usados, ele só precisa avançar um ou dois slots.

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

Jogue uma enumeração e um método de fábrica e terminamos 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 ser resolvido é 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 preguiçosamente recursos, mas na verdade não começa a reutilizar nenhum recurso até que o pool esteja cheio. Isso seria uma boa escolha se você deseja que o pool esteja cheio (o que parece ser o seu caso), mas quer adiar a despesa de realmente criá-los até o primeiro acesso (ou seja, para melhorar o tempo de inicialização).

Os métodos de carregamento realmente não são muito complicados, agora que temos a abstração da loja 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 sizee countacima se referem ao tamanho máximo do pool e ao número total de recursos pertencentes ao pool (mas não necessariamente disponíveis ), respectivamente. AcquireEageré o mais simples, assume que um item já está na loja - esses itens seriam pré-carregados na construção, ou seja, no PreloadItemsmétodo mostrado por último.

AcquireLazyverifica se há itens gratuitos no pool e, se não, ele cria um novo. AcquireLazyExpandingcriará um novo recurso, desde que o pool ainda não tenha atingido o tamanho desejado. Eu tentei otimizar isso para minimizar o bloqueio, e eu espero não ter cometido nenhum erro (eu ter testado esta em condições multi-threaded, mas, obviamente, não exaustivamente).

Você pode estar se perguntando por que nenhum desses métodos se incomoda em 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 particulares, alguns dos quais já foram mostrados:

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

Respondendo à pergunta que encobri no último parágrafo - como garantir a limitação do número total de recursos criados -, o .NET já possui uma ferramenta perfeitamente boa para isso, chamada Semaphore, e foi projetada especificamente para permitir uma correção número de threads que acessam um recurso (nesse caso, o "recurso" é o armazenamento interno de itens). Como não estamos implementando uma fila completa de produtores / consumidores, isso é perfeitamente adequado para nossas necessidades.

O construtor fica assim:

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

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

Como quase tudo já foi abstraído de maneira limpa até agora, os métodos Acquiree os Releasemétodos reais são realmente muito 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 explicado anteriormente, estamos usando o Semaphorepara 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á com o que se preocupar.

Por último, mas não menos importante, há a 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 objetivo dessa IsDisposedpropriedade ficará claro em um momento. Todo o Disposemétodo principal realmente faz é descartar os itens agrupados reais, se eles implementarem IDisposable.


Agora você pode basicamente usar isso como está, com um try-finallybloco, mas não gosto dessa sintaxe, porque se você começar a repassar recursos agrupados entre classes e métodos, isso ficará muito confuso. É possível que a classe principal que usa um recurso nem tenha uma referência ao pool. Ele realmente se torna bastante confuso, portanto, uma abordagem melhor é criar um objeto em pool "inteligente".

Digamos que começamos com a seguinte interface / classe 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á nosso Foorecurso descartável fingido que implementa IFooe possui algum código padrão para gerar identidades únicas. O que fazemos é criar outro objeto especial em pool:

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

Isso apenas copia todos os métodos "reais" para seu interior IFoo(poderíamos fazer isso com uma biblioteca de Proxy Dinâmico como o Castle, mas não vou entrar nisso). Ele também mantém uma referência ao Poolque o cria, para que, quando Disposeeste objeto, ele seja automaticamente liberado de volta ao pool. Exceto quando o pool já tiver sido descartado - isso significa que estamos no modo "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<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
    LoadingMode.Lazy, AccessMode.Circular);

// Sometime later on...
using (IFoo foo = pool.Acquire())
{
    foo.Test();
}

Isso é uma coisa muito boa de poder fazer. Isso significa que o código que usa o IFoo(ao contrário do código que o cria) não precisa realmente estar ciente do pool. Você pode até injetar IFoo objetos usando sua biblioteca DI favorita e Pool<T>como fornecedor / fábrica.


Coloquei o código completo no PasteBin para sua diversão de copiar e colar. Há também um pequeno programa de teste que você pode usar para brincar com diferentes modos de carregamento / acesso e condições multithread, para se certificar de que é seguro para threads e não com erros.

Entre em contato se tiver alguma dúvida ou preocupação sobre isso.

Aaronaught
fonte
62
Uma das respostas mais completas, úteis e interessantes que li no SO.
Josh Smeaton
Eu não poderia concordar mais com o @ Josh sobre essa resposta, especialmente para a parte PooledFoo, pois liberar os objetos sempre parecia ser tratado de uma maneira muito vazada e eu imaginava que faria mais sentido ter o uso possível do uso Construa como você mostrou que eu simplesmente não havia me sentado e tentado construir isso onde sua resposta me dá todas as informações de que preciso para resolver meu problema. Penso que, para minha situação específica, serei capaz de simplificar um pouco a maior parte do tempo, pois posso compartilhar as instâncias entre os threads e não precisar liberá-las novamente para o pool.
Marisic #
No entanto, se a abordagem simples não funcionar primeiro, tenho algumas idéias em minha mente sobre como lidar com a liberação de maneira inteligente no meu caso. Penso que, mais especificamente, estabeleceria o release para poder determinar que a sessão em si falhou e descartá-la e substituir uma nova na piscina. Independentemente desta publicação neste momento ser praticamente o guia definitivo sobre o pool de objetos no C # 3.0, estou ansioso para ver se mais alguém tem mais comentários sobre isso.
Chris Marisic
@ Chris: Se você está falando sobre proxies de clientes do WCF, também tenho um padrão para isso, embora você precise de um injetor de dependência ou um interceptador de métodos para usá-lo efetivamente. A versão DI usa o kernel com um provedor personalizado para obter uma versão nova se houver falha, a versão de interceptação de método (minha preferência) apenas envolve um proxy existente e insere uma verificação de falha antes de cada um. Não sei ao certo como seria fácil integrá-lo em um pool como este (ainda não tentei, pois acabei de escrever isso!), Mas seria definitivamente possível.
precisa saber é o seguinte
5
Muito impressionante, apesar de um pouco modificado demais para a maioria das situações. Eu esperaria que algo assim fizesse parte de uma estrutura.
precisa saber é o seguinte
7

Algo assim pode atender às suas necessidades.

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

                }
            }
        }
    }
}

Exemplo 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
fonte
1
Risque esse comentário anterior. Acho que achei estranho, porque esse pool não parece ter nenhum limite e, talvez não precise, isso depende dos requisitos.
Aaronaught
1
@Aaronaught - É realmente tão estranho? Eu queria criar um pool leve que ofereça apenas a funcionalidade necessária. Cabe ao cliente usar a classe corretamente.
precisa saber é o seguinte
1
+1 para uma solução muito simples que pode ser adaptada aos meus objetivos, alterando apenas o tipo de suporte para uma Lista / HashTable etc. e alterando o contador para rolar. Pergunta aleatória: como você lida com o gerenciamento do próprio objeto de pool? Você o coloca em um contêiner do COI que define como único ali?
Chris Marisic
1
Isso deve ser estático somente para leitura? Mas acho estranho que você colocaria uma declaração finalmente, se houver uma exceção, não seria provável que o objeto em si seja culpado? Você lidaria com isso dentro do Putmétodo e deixaria de fora por simplicidade algum tipo de verificação para verificar se o objeto está com defeito e criar uma nova instância a ser adicionada ao pool em vez de inserir a anterior?
Chris Marisic
1
@ Chris - estou simplesmente oferecendo uma ferramenta simples que achei útil no passado. O resto é com você. Modifique e use o código como achar melhor.
amigos estão dizendo sobre chaospandion
6

Exemplo do MSDN: Como: criar um pool de objetos usando um ConcurrentBag

Thomas Mutzl
fonte
Obrigado por esse link. Não há limite de tamanho para esta implementação; portanto, se houver um pico na criação de objetos, essas instâncias nunca serão coletadas e provavelmente nunca serão usadas até que ocorra outro pico. É muito simples e fácil de entender e não seria difícil adicionar um limite máximo de tamanho.
Muhammad Rehan Saeed
Agradável e simples
Daniel de Zwaan
4

Naquela época, a Microsoft fornecia uma estrutura por meio do Microsoft Transaction Server (MTS) e, mais tarde, do COM + para fazer o pool de objetos para objetos COM. Essa funcionalidade foi transportada para System.EnterpriseServices no .NET Framework e agora no Windows Communication Foundation.

Pool de Objetos no WCF

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

Pool de objetos .NET

Thomas
fonte
+1 por me mostrar que a IInstanceProviderinterface existe, pois implementarei isso na minha solução. Eu sempre gosto de empilhar meu código atrás de uma interface fornecida pela Microsoft quando eles fornecem uma definição adequada.
Chris Marisic
4

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

  1. Mude sync.WaitOne()para sync.WaitOne(timeout)e exponha o tempo limite como um parâmetro no Acquire(int timeout)método Isso também exigiria o tratamento da condição quando o encadeamento expirar, aguardando a disponibilidade de um objeto.
  2. Adicione um Recycle(T item)método para lidar com situações em que um objeto precise ser reciclado quando ocorrer uma falha, por exemplo.
Igor Pashchuk
fonte
3

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

public class ObjectPool<T>
    where T : class
{
    private readonly int maxSize;
    private Func<T> constructor;
    private int currentSize;
    private Queue<T> pool;
    private AutoResetEvent poolReleasedEvent;

    public ObjectPool(int maxSize, Func<T> constructor)
    {
        this.maxSize = maxSize;
        this.constructor = constructor;
        this.currentSize = 0;
        this.pool = new Queue<T>();
        this.poolReleasedEvent = new AutoResetEvent(false);
    }

    public T GetFromPool()
    {
        T item = null;
        do
        {
            lock (this)
            {
                if (this.pool.Count == 0)
                {
                    if (this.currentSize < this.maxSize)
                    {
                        item = this.constructor();
                        this.currentSize++;
                    }
                }
                else
                {
                    item = this.pool.Dequeue();
                }
            }

            if (null == item)
            {
                this.poolReleasedEvent.WaitOne();
            }
        }
        while (null == item);
        return item;
    }

    public void ReturnToPool(T item)
    {
        lock (this)
        {
            this.pool.Enqueue(item);
            this.poolReleasedEvent.Set();
        }
    }
}
Peter K.
fonte