Implémentation du modèle de regroupement d'objets C #

165

Quelqu'un a-t-il une bonne ressource sur la mise en œuvre d'une stratégie de pool d'objets partagés pour une ressource limitée dans la veine de la mise en commun de connexions SQL? (c'est-à-dire serait pleinement implémenté qu'il est thread-safe).

Pour faire le suivi de la demande de clarification @Aaronaught, l'utilisation du pool serait pour les demandes d'équilibrage de charge à un service externe. Pour le mettre dans un scénario qui serait probablement plus facile à comprendre immédiatement par opposition à ma situation directe. J'ai un objet de session qui fonctionne de manière similaire à l' ISessionobjet de NHibernate. Que chaque session unique gère sa connexion à la base de données. Actuellement, j'ai 1 objet de session de longue durée et je rencontre des problèmes où mon fournisseur de services limite mon utilisation de cette session individuelle.

En raison de leur manque d'espoir qu'une seule session serait traitée comme un compte de service de longue durée, ils la traitent apparemment comme un client qui martèle leur service. Ce qui m'amène à ma question ici, au lieu d'avoir 1 session individuelle, je créerais un pool de sessions différentes et répartirais les demandes au service sur ces multiples sessions au lieu de créer un seul point focal comme je le faisais auparavant.

Espérons que ce contexte offre une certaine valeur, mais pour répondre directement à certaines de vos questions:

Q: Les objets sont-ils coûteux à créer?
R: Aucun objet n'est un pool de ressources limitées

Q: Seront-ils acquis / libérés très fréquemment?
R: Oui, une fois de plus, ils peuvent être considérés comme NHibernate ISessions où 1 est généralement acquis et publié pour la durée de chaque demande de page.

Q: Un simple premier arrivé, premier servi suffira-t-il ou avez-vous besoin de quelque chose de plus intelligent, c'est-à-dire qui éviterait la famine?
R: Une simple distribution de type round robin suffirait, par famine, je suppose que vous voulez dire s'il n'y a pas de sessions disponibles, les appelants sont bloqués en attendant les versions. Ce n'est pas vraiment applicable car les sessions peuvent être partagées par différents appelants. Mon objectif est de répartir l'utilisation sur plusieurs sessions au lieu d'une seule session.

Je crois que c'est probablement une divergence par rapport à une utilisation normale d'un pool d'objets, c'est pourquoi j'ai initialement laissé cette partie de côté et prévu simplement d'adapter le modèle pour permettre le partage d'objets au lieu de permettre à une situation de famine de se produire.

Q: Qu'en est-il des choses comme les priorités, le chargement paresseux ou impatient, etc.?
R: Il n'y a pas de hiérarchisation, par souci de simplicité, supposons simplement que je créerais le pool d'objets disponibles lors de la création du pool lui-même.

Chris Marisic
la source
1
Pouvez-vous nous parler un peu de vos besoins? Tous les pools ne sont pas créés égaux. Les objets sont-ils coûteux à créer? Seront-ils acquis / libérés très fréquemment? Un simple premier arrivé, premier servi suffira-t-il ou avez-vous besoin de quelque chose de plus intelligent, c'est-à-dire qui éviterait la famine? Qu'en est-il des choses comme les priorités, le chargement paresseux ou impatient, etc.? Tout ce que vous pouvez ajouter nous aiderait (ou du moins moi) à trouver une réponse plus approfondie.
Aaronaught le
Chris - il suffit de regarder vos 2e et 3e paragraphes et de vous demander si ces sessions devraient vraiment être maintenues en vie indéfiniment? Cela ressemble à ce que votre fournisseur de services n'aime pas (sessions de longue durée), et vous recherchez peut-être une implémentation de pool qui lance de nouvelles sessions si nécessaire et les ferme lorsqu'elles ne sont pas utilisées (après une période spécifiée) . Cela peut être fait, mais c'est un peu plus compliqué, alors j'aimerais confirmer.
Aaronaught le
Je ne sais pas si j'ai besoin de cette solution robuste ou pas encore car ma solution est simplement hypothétique. Il est possible que mon fournisseur de services me mente simplement et que son service soit sur-vendu et ait simplement trouvé une excuse pour blâmer l'utilisateur.
Chris Marisic
1
Je pense que le TPL DataFlow BufferBlock fait la plupart de ce dont vous avez besoin.
dépensier
1
Le regroupement dans des environnements threadés est un problème récurrent, résolu par des modèles de conception tels que le pool de ressources et le cache de ressources. Consultez Architecture logicielle orientée modèle, Volume 3: Modèles pour la gestion des ressources pour plus d'informations.
Fuhrmanator

Réponses:

59

Regroupement d'objets dans .NET Core

Le noyau dotnet a une implémentation du pool d'objets ajoutée à la bibliothèque de classes de base (BCL). Vous pouvez lire le problème GitHub d'origine ici et afficher le code pour System.Buffers . Actuellement, il ArrayPools'agit du seul type disponible et utilisé pour regrouper les baies. Il y a un bon article de blog ici .

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 exemple de son utilisation peut être vu dans ASP.NET Core. Comme il se trouve dans le BCL dotnet core, ASP.NET Core peut partager son pool d'objets avec d'autres objets tels que le sérialiseur JSON de Newtonsoft.Json. Vous pouvez lire ce billet de blog pour plus d'informations sur la façon dont Newtonsoft.Json fait cela.

Regroupement d'objets dans le compilateur Microsoft Roslyn C #

Le nouveau compilateur Microsoft Roslyn C # contient le type ObjectPool , qui est utilisé pour regrouper les objets fréquemment utilisés qui seraient normalement remis en état et récupérés très souvent. Cela réduit la quantité et la taille des opérations de récupération de place qui doivent se produire. Il existe quelques sous-implémentations différentes utilisant toutes ObjectPool (voir: Pourquoi y a-t-il autant d'implémentations de pool d'objets dans Roslyn? ).

1 - SharedPools - Stocke un pool de 20 objets ou 100 si BigDefault est utilisé.

// 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 et StringBuilderPool - Des implémentations non strictement séparées mais des enveloppes autour de l'implémentation SharedPools illustrée ci-dessus spécifiquement pour List et StringBuilder. Donc, cela réutilise le pool d'objets stockés dans 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 et PooledHashSet - Ceux-ci utilisent ObjectPool directement et ont un pool d'objets totalement séparé. Stocke un pool de 128 objets.

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

Cette bibliothèque permet de regrouper les MemoryStreamobjets. C'est un remplacement instantané pour System.IO.MemoryStream. Il a exactement la même sémantique. Il a été conçu par les ingénieurs de Bing. Lisez l'article de blog ici ou consultez le code sur 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); 
}

Notez que cela RecyclableMemoryStreamManagerdevrait être déclaré une fois et qu'il vivra pendant tout le processus - c'est le pool. Il est parfaitement bien d'utiliser plusieurs piscines si vous le souhaitez.

Muhammad Rehan Saeed
la source
2
C'est une excellente réponse. Après que C # 6 et VS2015 soient RTM, je vais probablement en faire la réponse acceptée car c'est clairement la meilleure de toutes si elle est tellement réglée qu'elle est utilisée par Rosyln lui-même.
Chris Marisic
Je suis d'accord mais quelle implémentation utiliseriez-vous? Roslyn en contient trois. Voir le lien vers ma question dans la réponse.
Muhammad Rehan Saeed
1
On dirait que chacun a des objectifs très clairement définis, bien mieux que le seul choix d'une chaussure de taille unique à extrémité ouverte pour toutes.
Chris Marisic
1
@MuhammadRehanSaeed super ajout avec l'ArrayPool
Chris Marisic
1
Voir le RecyclableMemoryStreamc'est un ajout étonnant pour les optimisations ultra hautes performances.
Chris Marisic
315

Cette question est un peu plus délicate que ce à quoi on pourrait s'attendre en raison de plusieurs inconnues: le comportement de la ressource mise en pool, la durée de vie attendue / requise des objets, la vraie raison pour laquelle le pool est requis, etc. pools, pools de connexions, etc. - car il est plus facile d'en optimiser un lorsque vous savez exactement ce que fait la ressource et, plus important encore, contrôlez la manière dont cette ressource est mise en œuvre.

Comme ce n'est pas si simple, ce que j'ai essayé de faire est de proposer une approche assez flexible que vous pouvez expérimenter et voir ce qui fonctionne le mieux. Toutes mes excuses à l'avance pour le long message, mais il y a beaucoup de chemin à parcourir pour mettre en œuvre un pool de ressources à usage général décent. et je ne fais que gratter la surface.

Une piscine à usage général devrait avoir quelques "paramètres" principaux, notamment:

  • Stratégie de chargement des ressources - impatient ou paresseux;
  • Mécanisme de chargement des ressources - comment en construire un;
  • Stratégie d'accès - vous mentionnez "round robin" qui n'est pas aussi simple qu'il y paraît; cette implémentation peut utiliser un tampon circulaire qui est similaire , mais pas parfait, car le pool n'a aucun contrôle sur le moment où les ressources sont réellement récupérées. Les autres options sont FIFO et LIFO; Le FIFO aura davantage un modèle d'accès aléatoire, mais LIFO facilite considérablement la mise en œuvre d'une stratégie de libération des moins récemment utilisées (qui, selon vous, était hors de portée, mais cela vaut toujours la peine d'être mentionné).

Pour le mécanisme de chargement des ressources, .NET nous donne déjà une abstraction propre - les délégués.

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

Passez ceci à travers le constructeur de la piscine et nous en avons presque terminé. L'utilisation d'un type générique avec une new()contrainte fonctionne également, mais c'est plus flexible.


Parmi les deux autres paramètres, la stratégie d'accès est la bête la plus compliquée, donc mon approche consistait à utiliser une approche basée sur l'héritage (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; }
    }
}

Le concept ici est simple - nous laisserons la Poolclasse publique gérer les problèmes courants tels que la sécurité des threads, mais utiliserons un "magasin d'objets" différent pour chaque modèle d'accès. LIFO est facilement représenté par une pile, FIFO est une file d'attente, et j'ai utilisé une implémentation de tampon circulaire pas très optimisée mais probablement adéquate en utilisant un List<T>pointeur d'index et pour approximer un modèle d'accès circulaire .

Toutes les classes ci-dessous sont des classes internes de Pool<T>- c'était un choix de style, mais comme elles ne sont vraiment pas destinées à être utilisées en dehors de Pool, cela a le plus de sens.

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

Ce sont les plus évidents - pile et file d'attente. Je ne pense pas qu'ils méritent vraiment beaucoup d'explications. Le tampon circulaire est un peu plus compliqué:

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

J'aurais pu choisir un certain nombre d'approches différentes, mais l'essentiel est que les ressources doivent être accessibles dans le même ordre où elles ont été créées, ce qui signifie que nous devons conserver les références à celles-ci, mais les marquer comme "en cours d'utilisation" (ou non ). Dans le pire des cas, un seul emplacement est toujours disponible, et il faut une itération complète du tampon pour chaque extraction. C'est mauvais si vous avez des centaines de ressources mises en commun et que vous les acquérez et les libérez plusieurs fois par seconde; ce n'est pas vraiment un problème pour un pool de 5 à 10 éléments, et dans le cas typique , où les ressources sont peu utilisées, il suffit d'avancer d'un ou deux emplacements.

N'oubliez pas que ces classes sont des classes internes privées - c'est pourquoi elles n'ont pas besoin de beaucoup de vérification d'erreurs, le pool lui-même en restreint l'accès.

Ajoutez une énumération et une méthode d'usine et nous en avons terminé avec cette partie:

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

Le prochain problème à résoudre est la stratégie de chargement. J'ai défini trois types:

public enum LoadingMode { Eager, Lazy, LazyExpanding };

Les deux premiers devraient être explicites; le troisième est une sorte d'hybride, il charge les ressources paresseusement mais ne commence pas à réutiliser les ressources tant que le pool n'est pas plein. Ce serait un bon compromis si vous voulez que la piscine soit pleine (ce qui semble être le cas) mais que vous voulez reporter les frais de création effective jusqu'au premier accès (c'est-à-dire pour améliorer les temps de démarrage).

Les méthodes de chargement ne sont vraiment pas trop compliquées, maintenant que nous avons l'abstraction du magasin d'objets:

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

Les champs sizeet countci-dessus font référence respectivement à la taille maximale du pool et au nombre total de ressources détenues par le pool (mais pas nécessairement disponibles ). AcquireEagerest le plus simple, il suppose qu'un article est déjà dans le magasin - ces articles seraient préchargés à la construction, c'est-à-dire dans la PreloadItemsméthode indiquée en dernier.

AcquireLazyvérifie s'il y a des articles gratuits dans le pool, et sinon, il en crée un nouveau. AcquireLazyExpandingcréera une nouvelle ressource tant que le pool n'a pas encore atteint sa taille cible. J'ai essayé d'optimiser ce pour minimiser le verrouillage, et j'espère que je ne l' ai pas fait d'erreur (je l' ai testé dans des conditions multi-thread, mais évidemment pas exhaustive).

Vous vous demandez peut-être pourquoi aucune de ces méthodes ne se soucie de vérifier si le magasin a atteint ou non la taille maximale. J'y reviendrai dans un instant.


Maintenant pour la piscine elle-même. Voici l'ensemble complet des données privées, dont certaines ont déjà été montrées:

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

En réponse à la question que j'ai passée sous silence dans le dernier paragraphe - comment nous assurer de limiter le nombre total de ressources créées - il s'avère que le .NET dispose déjà d'un très bon outil pour cela, il s'appelle Semaphore et il est spécifiquement conçu pour permettre un nombre de threads accédant à une ressource (dans ce cas, la "ressource" est le magasin d'objets interne). Puisque nous n'implémentons pas une file d'attente complète de producteurs / consommateurs, cela répond parfaitement à nos besoins.

Le constructeur ressemble à ceci:

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

Ne devrait pas y avoir de surprises ici. La seule chose à noter est le boîtier spécial pour un chargement hâtif, en utilisant la PreloadItemsméthode déjà montrée précédemment.

Étant donné que presque tout a été proprement abrégé maintenant, le réel Acquireet les Releaseméthodes sont vraiment très simples:

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

Comme expliqué précédemment, nous utilisons le Semaphorepour contrôler la concurrence au lieu de vérifier religieusement l'état du magasin d'objets. Tant que les objets acquis sont correctement libérés, il n'y a rien à craindre.

Dernier point mais non le moindre, il y a le nettoyage:

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

Le but de cette IsDisposedpropriété deviendra clair dans un instant. Tout ce que la Disposeméthode principale fait vraiment est de supprimer les éléments mis en commun s'ils sont implémentés IDisposable.


Maintenant, vous pouvez essentiellement l'utiliser tel try-finallyquel , avec un bloc, mais je n'aime pas cette syntaxe, car si vous commencez à faire passer des ressources mises en commun entre les classes et les méthodes, cela deviendra très déroutant. Il est possible que la classe principale qui utilise une ressource ne même pas avoir une référence à la piscine. Cela devient vraiment assez compliqué, donc une meilleure approche consiste à créer un objet groupé «intelligent».

Disons que nous commençons avec l'interface / classe simple suivante:

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

Voici notre prétendue Fooressource jetable qui implémente IFooet contient un code standard pour générer des identités uniques. Ce que nous faisons est de créer un autre objet spécial et groupé:

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

Cela renvoie simplement toutes les méthodes «réelles» à son intérieur IFoo(nous pourrions le faire avec une bibliothèque Dynamic Proxy comme Castle, mais je n'entrerai pas dans cela). Il conserve également une référence à celui Poolqui le crée, de sorte que lorsque nous Disposecet objet, il se libère automatiquement dans le pool. Sauf lorsque le pool a déjà été éliminé - cela signifie que nous sommes en mode "nettoyage" et dans ce cas, il nettoie en fait la ressource interne à la place.


En utilisant l'approche ci-dessus, nous arrivons à écrire du code comme celui-ci:

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

C'est une très bonne chose à pouvoir faire. Cela signifie que le code qui utilise le IFoo(par opposition au code qui le crée) n'a pas réellement besoin de connaître le pool. Vous pouvez même injecter des IFoo objets en utilisant votre bibliothèque DI préférée et en Pool<T>tant que fournisseur / usine.


J'ai mis le code complet sur PasteBin pour votre plaisir de copier-coller. Il existe également un court programme de test que vous pouvez utiliser pour jouer avec différents modes de chargement / accès et des conditions multithreads, pour vous assurer qu'il est thread-safe et non bogué.

Faites-moi savoir si vous avez des questions ou des préoccupations à ce sujet.

Aaronaught
la source
62
L'une des réponses les plus complètes, utiles et intéressantes que j'ai lues sur SO.
Josh Smeaton
Je ne pourrais pas être plus d'accord avec @Josh sur cette réponse, en particulier pour la partie PooledFoo car la libération des objets semblait toujours être gérée de manière très fuyante et j'avais imaginé qu'il serait plus logique de pouvoir utiliser l'utilisation Construisez comme vous l'avez montré que je ne m'étais pas assis et j'ai essayé de construire cela là où votre réponse me donne toutes les informations dont je pourrais avoir besoin pour résoudre mon problème. Je pense que pour ma situation spécifique, je serai en mesure de simplifier un peu cela, car je peux partager les instances entre les threads et je n'ai pas besoin de les relâcher dans le pool.
Chris Marisic
Cependant, si l'approche simple ne fonctionne pas d'abord, j'ai quelques idées dans ma tête sur la façon dont je pourrais gérer intelligemment la libération pour mon cas. Je pense que plus précisément, j'établirais la version pour être en mesure de déterminer que la session elle-même est défaillante, de la disposer et de la remplacer par une nouvelle dans le pool. Indépendamment de ce post à ce stade est à peu près le guide définitif sur la mise en commun d'objets dans C # 3.0, j'ai hâte de voir si quelqu'un d'autre a plus de commentaires à ce sujet.
Chris Marisic
@Chris: Si vous parlez de proxys client WCF, j'ai également un modèle pour cela, bien que vous ayez besoin d'un injecteur de dépendances ou d'un intercepteur de méthode pour l'utiliser efficacement. La version DI utilise le noyau avec un fournisseur personnalisé pour obtenir une version new-if-faulted, la version d'interception de méthode (ma préférence) encapsule juste un proxy existant et insère une vérification des pannes avant chacun. Je ne sais pas à quel point il serait facile de l'intégrer dans un pool comme celui-ci (je n'ai pas vraiment essayé, puisque je viens d'écrire ceci!) Mais ce serait certainement possible.
Aaronaught le
5
Très impressionnant, bien qu'un peu sur-conçu pour la plupart des situations. Je m'attendrais à ce que quelque chose comme ça fasse partie d'un cadre.
ChaosPandion
7

Quelque chose comme ça pourrait répondre à vos besoins.

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

                }
            }
        }
    }
}

Exemple d'utilisation

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
la source
1
Grattez ce commentaire précédent. Je pense que j'ai juste trouvé cela étrange parce que ce pool ne semble pas avoir de seuils, et peut-être qu'il n'en a pas besoin, cela dépendrait des exigences.
Aaronaught le
1
@Aaronaught - Est-ce vraiment si étrange? Je voulais créer une piscine légère qui offre juste les fonctionnalités nécessaires. Il appartient au client d'utiliser correctement la classe.
ChaosPandion
1
+1 pour une solution très simple qui peut être adaptée à mes objectifs en changeant simplement le type de support pour être une liste / table de hachage, etc. et en changeant le compteur pour qu'il se retourne. Question aléatoire comment gérez-vous la gestion de l'objet pool lui-même? Le collez-vous simplement dans un conteneur IOC le définissant comme singleton?
Chris Marisic
1
Cela devrait-il être statique en lecture seule? Mais je trouve étrange que vous ayez mis dans une déclaration finally, s'il y a une exception, ne serait-il pas probable que l'objet lui-même soit défectueux? Souhaitez-vous gérer cela à l'intérieur de la Putméthode et laisser de côté pour plus de simplicité un certain type de vérification de si l'objet est défectueux et de créer une nouvelle instance à ajouter au pool au lieu d'insérer la précédente?
Chris Marisic
1
@Chris - Je propose simplement un outil simple que j'ai trouvé utile dans le passé. Le reste dépend de toi. Modifiez et utilisez le code comme bon vous semble.
ChaosPandion
6

Exemple de MSDN: Comment: créer un pool d'objets à l'aide d'un ConcurrentBag

Thomas Mutzl
la source
Merci pour ce lien. Il n'y a pas de limite de taille pour cette implémentation, donc si vous avez un pic dans la création d'objets, ces instances ne seront jamais collectées et probablement jamais utilisées jusqu'à ce qu'il y ait un autre pic. C'est un moyen très simple et facile à comprendre et il ne serait pas difficile d'ajouter une limite de taille maximale.
Muhammad Rehan Saeed
Nice and simple
Daniel de Zwaan
4

À l'époque, Microsoft fournissait un cadre via Microsoft Transaction Server (MTS) et plus tard COM + pour effectuer le regroupement d'objets pour les objets COM. Cette fonctionnalité a été transférée à System.EnterpriseServices dans le .NET Framework et maintenant dans Windows Communication Foundation.

Regroupement d'objets dans WCF

Cet article provient de .NET 1.1 mais doit toujours s'appliquer dans les versions actuelles du Framework (même si WCF est la méthode préférée).

Regroupement d'objets .NET

Thomas
la source
+1 pour m'avoir montré que l' IInstanceProviderinterface existe car je vais l'implémenter pour ma solution. Je suis toujours fan de l'empilement de mon code derrière une interface fournie par Microsoft lorsqu'ils fournissent une définition appropriée.
Chris Marisic
4

J'aime vraiment l'implémentation d'Aronaught - d'autant plus qu'il gère l'attente de la disponibilité des ressources grâce à l'utilisation d'un sémaphore. Il y a plusieurs ajouts que je voudrais faire:

  1. Changement sync.WaitOne()de sync.WaitOne(timeout)et exposer le délai d' attente en tant que paramètre sur la Acquire(int timeout)méthode. Cela nécessiterait également de gérer la condition lorsque le thread expire en attendant qu'un objet devienne disponible.
  2. Ajoutez une Recycle(T item)méthode pour gérer les situations où un objet doit être recyclé en cas de panne, par exemple.
Igor Pashchuk
la source
3

Il s'agit d'une autre implémentation, avec un nombre limité d'objets dans le 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.
la source