Pas de ConcurrentList <T> dans .Net 4.0?


198

J'étais ravi de voir le nouvel System.Collections.Concurrentespace de noms dans .Net 4.0, plutôt sympa! Je l' ai vu ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBaget BlockingCollection.

Une chose qui semble mystérieusement manquer est a ConcurrentList<T>. Dois-je l'écrire moi-même (ou le retirer du Web :))?

Suis-je en train de manquer quelque chose d'évident ici?



4
@RodrigoReis, ConcurrentBag <T> est une collection non ordonnée, tandis que List <T> est commandé.
Adam Calvet Bohl, le

4
Comment pourriez-vous éventuellement avoir une collection ordonnée dans un environnement multithread? Vous n'auriez jamais le contrôle de la séquence d'éléments, par conception.
Jeremy Holovacs du

Utilisez un verrou à la place
Erik Bergstedt

il y a un fichier appelé ThreadSafeList.cs dans le code source dotnet qui ressemble beaucoup à du code ci-dessous. Il utilise également ReaderWriterLockSlim et essayait de comprendre pourquoi utiliser cela au lieu d'un simple verrou (obj)?
colin lamarre

Réponses:


166

Je l'ai essayé il y a quelque temps (aussi: sur GitHub ). Mon implémentation a eu quelques problèmes, que je n'entrerai pas ici. Permettez-moi de vous dire, plus important encore, ce que j'ai appris.

Tout d'abord, il n'y a aucun moyen d'obtenir une implémentation complète de IList<T> qui soit sans verrou et sans thread. En particulier, les insertions et les suppressions aléatoires ne fonctionneront pas, sauf si vous oubliez également l'accès aléatoire O (1) (c'est-à-dire, sauf si vous "trichez" et utilisez simplement une sorte de liste chaînée et laissez l'indexation aspirer).

Ce que je pensais peut - être utile était un thread-safe, sous - ensemble limité de IList<T>: en particulier, qui permettrait à un Addet de fournir au hasard en lecture seule l' accès par index (mais non Insert, RemoveAtetc., et pas non plus au hasard écrire un accès).

C'était le but de ma ConcurrentList<T>mise en œuvre . Mais lorsque j'ai testé ses performances dans des scénarios multithread, j'ai constaté que la simple synchronisation s'ajoute à a List<T>était plus rapide . Fondamentalement, l'ajout à un List<T>est déjà rapide comme l'éclair; la complexité des étapes de calcul impliquées est minuscule (incrémenter un index et l'assigner à un élément dans un tableau; c'est vraiment ça ). Vous auriez besoin d'une tonne d'écritures simultanées pour voir toute sorte de conflit de verrouillage à ce sujet; et même dans ce cas, les performances moyennes de chaque écriture l'emporteraient toujours sur l'implémentation sans verrou plus coûteuse ConcurrentList<T>.

Dans le cas relativement rare où le tableau interne de la liste doit se redimensionner, vous payez un petit coût. Donc, finalement, j'ai conclu que c'était le seul scénario de niche où un ConcurrentList<T>type de collection à ajouter uniquement aurait du sens: lorsque vous voulez garantir une faible surcharge de l'ajout d'un élément sur chaque appel (donc, par opposition à un objectif de performance amorti).

Ce n'est tout simplement pas une classe aussi utile que vous le pensez.


52
Et si vous avez besoin de quelque chose de similaire à List<T>celui qui utilise une synchronisation basée sur un ancien moniteur, il y en a SynchronizedCollection<T>dans la BCL: msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH

8
Un petit ajout: utilisez le paramètre du constructeur de capacité pour éviter (autant que possible) le scénario de redimensionnement.
Henk Holterman

2
Le plus grand scénario où a ConcurrentListserait une victoire serait quand il n'y a pas beaucoup d'activité à ajouter à la liste, mais il y a beaucoup de lecteurs simultanés. On pourrait réduire les frais généraux des lecteurs à une seule barrière de mémoire (et même éliminer cela si les lecteurs n'étaient pas préoccupés par des données légèrement périmées).
supercat

2
@Kevin: Il est assez trivial de construire un ConcurrentList<T>de telle manière que les lecteurs soient garantis de voir un état cohérent sans avoir besoin d'aucun verrouillage, avec une surcharge supplémentaire relativement légère. Lorsque la liste s'étend, par exemple de la taille 32 à 64, conservez le tableau size-32 et créez un nouveau tableau size-64. Lors de l'ajout de chacun des 32 éléments suivants, placez-le dans l'emplacement 32-63 du nouveau tableau et copiez un ancien élément du tableau taille-32 dans le nouveau. Jusqu'à ce que le 64ème élément soit ajouté, les lecteurs chercheront dans le tableau taille-32 pour les éléments 0-31 et dans le tableau taille-64 pour les éléments 32-63.
supercat

2
Une fois le 64e élément ajouté, le tableau de taille 32 fonctionnera toujours pour récupérer les éléments 0 à 31, mais les lecteurs n'auront plus besoin de l'utiliser. Ils peuvent utiliser le tableau taille-64 pour tous les éléments 0-63 et un tableau taille-128 pour les éléments 64-127. Le surcoût de la sélection de l'un des deux tableaux à utiliser, plus une barrière de mémoire si vous le souhaitez, serait inférieur au surcoût du verrou de lecture-écriture même le plus efficace imaginable. Les écritures devraient probablement utiliser des verrous (sans verrou serait possible, surtout si cela ne vous dérangeait pas de créer une nouvelle instance d'objet à chaque insertion, mais le verrou devrait être bon marché.
supercat

38

Pour quoi utiliseriez-vous une ConcurrentList?

Le concept d'un conteneur à accès aléatoire dans un monde fileté n'est pas aussi utile qu'il y paraît. La déclaration

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

dans son ensemble ne serait toujours pas thread-safe.

Au lieu de créer une liste concurrente, essayez de créer des solutions avec ce qui existe. Les classes les plus courantes sont le ConcurrentBag et en particulier le BlockingCollection.


Bon point. Mais ce que je fais est un peu plus banal. J'essaie simplement d'assigner le ConcurrentBag <T> dans un IList <T>. Je pourrais basculer ma propriété vers un IEnumerable <T>, mais je ne peux pas y ajouter des éléments.
Alan

1
@Alan: Il n'y a aucun moyen de l'implémenter sans verrouiller la liste. Étant donné que vous pouvez déjà utiliser Monitorpour le faire de toute façon, il n'y a aucune raison pour une liste simultanée.
Billy ONeal

6
@dcp - oui, ce n'est pas intrinsèquement sûr pour les threads. ConcurrentDictionary possède des méthodes qui font en une seule opération atomique, comme AddOrUpdate, GetOrAdd, TryUpdate, etc. Ils ont encore ContainsKey parce que parfois vous voulez juste savoir si la clé est là sans modifier le dictionnaire (pensez HashSet)
Zarat

3
@dcp - ContainsKey est threadsafe par lui-même, votre exemple (pas ContainsKey!) a juste une condition de concurrence car vous effectuez un deuxième appel en fonction de la première décision, qui peut à ce moment-là déjà être obsolète.
Zarat

2
Henk, je ne suis pas d'accord. Je pense qu'il existe un scénario simple où cela pourrait être très utile. Le thread de travail écrit dedans va lire le thread UI et mettre à jour l'interface en conséquence. Si vous souhaitez ajouter un élément de manière triée, il faudra une écriture à accès aléatoire. Vous pouvez également utiliser une pile et une vue des données mais vous devrez maintenir 2 collections :-(.
Eric Ouellet

19

Avec tout le respect que je dois aux bonnes réponses déjà fournies, il y a des moments où je veux simplement une IList thread-safe. Rien d'avancé ou de fantaisie. Les performances sont importantes dans de nombreux cas, mais parfois ce n'est pas un problème. Oui, il y aura toujours des défis sans méthodes comme "TryGetValue", etc., mais la plupart des cas, je veux juste quelque chose que je peux énumérer sans avoir à se soucier de mettre des verrous autour de tout. Et oui, quelqu'un peut probablement trouver un "bogue" dans mon implémentation qui pourrait conduire à un blocage ou quelque chose (je suppose), mais soyons honnêtes: en ce qui concerne le multithread, si vous n'écrivez pas correctement votre code, il va de toute façon à l'impasse. Dans cet esprit, j'ai décidé de faire une implémentation ConcurrentList simple qui répond à ces besoins de base.

Et pour ce que ça vaut: j'ai fait un test de base pour ajouter 10 000 000 d'éléments à List et ConcurrentList standard et les résultats ont été:

Liste terminée en: 7793 millisecondes. Concurrent terminé en: 8064 millisecondes.

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}

5
OK, ancienne réponse mais toujours: RemoveAt(int index)n'est jamais Insert(int index, T item)sûr pour les threads, n'est sûr que pour l'index == 0, le retour de IndexOf()est immédiatement obsolète, etc. Ne commencez même pas à propos de this[int].
Henk Holterman

2
Et vous n'avez pas besoin et ne voulez pas d'un ~ Finalizer ().
Henk Holterman

2
Vous dites que vous avez renoncé à empêcher l'éventualité d'un blocage - et qu'un seul ReaderWriterLockSlimpeut être bloqué facilement en utilisant EnterUpgradeableReadLock()simultanément. Cependant, vous ne l'utilisez pas, vous ne rendez pas le verrou accessible à l'extérieur, et vous n'appelez pas par exemple une méthode qui entre dans un verrou en écriture tout en maintenant un verrou en lecture, donc l'utilisation de votre classe ne fait plus de blocages probable.
Eugene Beresovsky

1
L'interface non simultanée n'est pas appropriée pour un accès simultané. Par exemple, ce qui suit n'est pas atomique var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. En général, tout combo lecture-écriture peut vous causer de gros problèmes lorsqu'il est effectué simultanément. C'est pourquoi les structures de données simultanées fournissent généralement des méthodes pour celles-ci, comme ConcurrentDictionaryles AddOrGetetc. NB Votre répétition constante (et redondante car les membres sont déjà marqués comme tels par le trait de soulignement) des this.encombrements.
Eugene Beresovsky

1
Merci Eugene. Je suis un grand utilisateur de .NET Reflector qui met "ceci". sur tous les champs non statiques. En tant que tel, j'ai grandi pour préférer la même chose. En ce qui concerne cette interface non simultanée qui n'est pas appropriée: vous avez tout à fait raison de dire que tenter d'effectuer plusieurs actions contre mon implémentation peut devenir peu fiable. Mais l'exigence ici est simplement que des actions uniques (ajouter, supprimer, effacer ou énumération) peuvent être effectuées sans corrompre la collection. Il supprime essentiellement la nécessité de mettre des instructions de verrouillage autour de tout.
Brian Booth

11

ConcurrentList(comme un tableau redimensionnable, pas une liste chaînée) n'est pas facile à écrire avec des opérations non bloquantes. Son API ne se traduit pas bien en une version "simultanée".


12
Ce n'est pas seulement difficile à écrire, il est même difficile de trouver une interface utile.
CodesInChaos

11

La raison pour laquelle il n'y a pas de ConcurrentList est qu'elle ne peut fondamentalement pas être écrite. La raison en est que plusieurs opérations importantes dans IList reposent sur des indices, et cela ne fonctionnera tout simplement pas. Par exemple:

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

L'effet recherché par l'auteur est d'insérer "chien" avant "chat", mais dans un environnement multithread, tout peut arriver à la liste entre ces deux lignes de code. Par exemple, un autre thread pourrait faire list.RemoveAt(0), en déplaçant la liste entière vers la gauche, mais surtout, catIndex ne changera pas. L'impact ici est que l' Insertopération mettra en fait le «chien» après le chat, pas avant lui.

Les nombreuses implémentations que vous voyez comme «réponses» à cette question sont bien intentionnées, mais comme le montre ce qui précède, elles n'offrent pas de résultats fiables. Si vous voulez vraiment une sémantique de type liste dans un environnement multithread, vous ne pouvez pas y arriver en mettant des verrous à l' intérieur les méthodes d'implémentation de la liste. Vous devez vous assurer que tout index que vous utilisez vit entièrement dans le contexte du verrou. Le résultat est que vous pouvez utiliser une liste dans un environnement multithread avec le bon verrouillage, mais la liste elle-même ne peut pas être créée pour exister dans ce monde.

Si vous pensez avoir besoin d'une liste simultanée, il n'y a vraiment que deux possibilités:

  1. Ce dont vous avez vraiment besoin, c'est d'un ConcurrentBag
  2. Vous devez créer votre propre collection, peut-être implémentée avec une liste et votre propre contrôle de concurrence.

Si vous avez un ConcurrentBag et que vous devez le passer en tant qu'IList, vous avez un problème, car la méthode que vous appelez a spécifié qu'ils pourraient essayer de faire quelque chose comme je l'ai fait ci-dessus avec le chat & chien. Dans la plupart des mondes, cela signifie que la méthode que vous appelez n'est tout simplement pas conçue pour fonctionner dans un environnement multithread. Cela signifie que vous devez le refactoriser pour qu'il soit ou, si vous ne le pouvez pas, vous devrez le gérer très soigneusement. Vous devrez presque certainement créer votre propre collection avec ses propres verrous et appeler la méthode incriminée dans un verrou.


5

Dans les cas où les lectures sont beaucoup plus nombreuses que les écritures ou (même si elles sont fréquentes) les écritures ne sont pas simultanées , une approche de copie sur écriture peut être appropriée.

L'implémentation illustrée ci-dessous est

  • sans verrou
  • incroyablement rapide pour les lectures simultanées , même si les modifications simultanées sont en cours - peu importe le temps qu'elles prennent
  • parce que les "instantanés" sont immuables, une atomicité sans verrou est possible, c'est-à-dire var snap = _list; snap[snap.Count - 1];qu'elle ne lancera jamais (enfin, sauf pour une liste vide bien sûr), et vous obtenez également une énumération thread-safe avec la sémantique des instantanés gratuitement .. comment j'aime l'immuabilité!
  • implémenté de manière générique , applicable à toute structure de données et tout type de modification
  • mort simple , c'est-à-dire facile à tester, à déboguer, à vérifier en lisant le code
  • utilisable dans .Net 3.5

Pour que la copie sur écriture fonctionne, vous devez conserver vos structures de données effectivement immuables , c'est-à-dire que personne n'est autorisé à les modifier après les avoir mises à la disposition d'autres threads. Lorsque vous souhaitez modifier, vous

  1. cloner la structure
  2. faire des modifications sur le clone
  3. permuter atomiquement dans la référence au clone modifié

Code

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

Usage

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

Si vous avez besoin de plus de performances, cela aidera à dégénérer la méthode, par exemple créer une méthode pour chaque type de modification (Ajouter, Supprimer, ...) que vous voulez, et coder en dur les pointeurs de fonction cloneret op.

NB # 1 Il est de votre responsabilité de vous assurer que personne ne modifie la structure de données (supposée) immuable. Il n'y a rien que nous puissions faire dans une implémentation générique pour empêcher cela, mais lors de la spécialisation List<T>, vous pouvez vous prémunir contre les modifications à l'aide de List.AsReadOnly ()

NB # 2 Faites attention aux valeurs de la liste. L'approche de copie en écriture ci-dessus protège uniquement leur appartenance à la liste, mais si vous ne mettez pas de chaînes, mais d'autres objets modifiables, vous devez prendre soin de la sécurité des threads (par exemple, le verrouillage). Mais cela est orthogonal à cette solution et par exemple le verrouillage des valeurs mutables peut être facilement utilisé sans problèmes. Vous devez juste en être conscient.

NB # 3 Si votre structure de données est énorme et que vous la modifiez fréquemment, l'approche copier-tout-sur-écriture peut être prohibitive à la fois en termes de consommation de mémoire et de coût CPU de copie. Dans ce cas, vous pouvez utiliser à la place les collections immuables de MS .


3

System.Collections.Generic.List<t>est déjà thread-safe pour plusieurs lecteurs. Essayer de le rendre sûr pour les threads pour plusieurs écrivains n'aurait aucun sens. (Pour des raisons que Henk et Stephen ont déjà mentionnées)


Vous ne pouvez pas voir un scénario où je pourrais avoir 5 threads à ajouter à une liste? De cette façon, vous pouvez voir la liste accumuler des enregistrements avant même qu'ils ne se terminent tous.
Alan

9
@Alan - ce serait un ConcurrentQueue, ConcurrentStack ou ConcurrentBag. Pour donner un sens à une liste concurrente, vous devez fournir un cas d'utilisation où les classes disponibles ne sont pas suffisantes. Je ne vois pas pourquoi je voudrais un accès indexé lorsque les éléments des index peuvent changer de manière aléatoire par des suppressions simultanées. Et pour une lecture "verrouillée", vous pouvez déjà prendre des instantanés des classes simultanées existantes et les mettre dans une liste.
Zarat

Vous avez raison - je ne veux pas d'un accès indexé. J'utilise généralement IList <T> comme proxy pour un IEnumerable auquel je peux .Add (T) de nouveaux éléments. C'est de là que vient la question, vraiment.
Alan

@Alan: Alors vous voulez une file d'attente, pas une liste.
Billy ONeal

3
Je pense que tu as tort. Dire: sûr pour plusieurs lecteurs ne signifie pas que vous ne pouvez pas écrire en même temps. Écrire signifierait également supprimer et vous obtiendrez une erreur si vous supprimez pendant l'itération.
Eric Ouellet

2

Certaines personnes ont signalé certains points positifs (et certaines de mes pensées):

  • Il peut sembler fou à un accesseur aléatoire (indexeur) incapable, mais pour moi, cela semble bien. Il suffit de penser qu'il existe de nombreuses méthodes sur les collections multi-thread qui pourraient échouer comme Indexer et Delete. Vous pouvez également définir une action d'échec (repli) pour l'accesseur d'écriture comme «échouer» ou simplement «ajouter à la fin».
  • Ce n'est pas parce que c'est une collection multithread qu'elle sera toujours utilisée dans un contexte multithread. Ou il pourrait également être utilisé par un seul écrivain et un seul lecteur.
  • Une autre façon de pouvoir utiliser l'indexeur de manière sûre pourrait être d'encapsuler des actions dans un verrou de la collection en utilisant sa racine (si elle est rendue publique).
  • Pour de nombreuses personnes, rendre un rootLock visible devient une "bonne pratique". Je ne suis pas sûr à 100% de ce point car s'il est caché, vous supprimez beaucoup de flexibilité pour l'utilisateur. Nous devons toujours nous rappeler que la programmation multithread n'est pas pour tout le monde. Nous ne pouvons pas empêcher toutes sortes de mauvaises utilisations.
  • Microsoft devra faire un certain travail et définir une nouvelle norme pour introduire une utilisation appropriée de la collection multithread. Tout d'abord, l'IEnumerator ne doit pas avoir de moveNext mais doit avoir un GetNext qui retourne vrai ou faux et obtient un paramètre de sortie de type T (de cette façon, l'itération ne serait plus bloquante). De plus, Microsoft utilise déjà "using" en interne dans foreach, mais utilise parfois directement IEnumerator sans l'encapsuler avec "using" (un bogue dans la vue de la collection et probablement à plus d'endroits) - L'utilisation d'encapsulation d'IEnumerator est une pratique recommandée par Microsoft. Ce bug supprime un bon potentiel d'itérateur sûr ... Itérateur qui verrouille la collection dans le constructeur et déverrouille sur sa méthode Dispose - pour une méthode de blocage foreach.

Ce n'est pas une réponse. Ce ne sont que des commentaires qui ne correspondent pas vraiment à un endroit spécifique.

... Ma conclusion, Microsoft doit apporter des modifications profondes à la "foreach" pour rendre la collection MultiThreaded plus facile à utiliser. Il doit également suivre ses propres règles d'utilisation IEnumerator. Jusque-là, nous pouvons facilement écrire une MultiThreadList qui utiliserait un itérateur de blocage mais qui ne suivra pas "IList". Au lieu de cela, vous devrez définir votre propre interface "IListPersonnal" qui pourrait échouer sur "insert", "remove" et accesseur aléatoire (indexeur) sans exception. Mais qui voudra l'utiliser s'il n'est pas standard?


On pourrait facilement écrire un ConcurrentOrderedBag<T>qui inclurait une implémentation en lecture seule de IList<T>, mais offrirait également une int Add(T value)méthode entièrement thread-safe . Je ne vois pas pourquoi des ForEachchangements seraient nécessaires. Bien que Microsoft ne le dise pas explicitement, leur pratique suggère qu'il est parfaitement acceptable IEnumerator<T>d'énumérer le contenu de la collection qui existait lors de sa création; l'exception de modification de collection n'est requise que si l'énumérateur n'est pas en mesure de garantir un fonctionnement sans problème.
supercat

En parcourant une collection MT, la façon dont elle est conçue pourrait conduire, comme vous l'avez dit, à une exception ... Laquelle je ne connais pas. Pourriez-vous piéger toutes les exceptions? Dans mon propre livre, l'exception est une exception et ne devrait pas se produire dans l'exécution normale du code. Sinon, pour empêcher une exception, vous devez verrouiller la collection ou obtenir une copie (de manière sûre, c'est-à-dire verrouiller) ou implémenter un mécanisme très complexe dans la collection pour empêcher qu'une exception ne se produise en raison de la concurrence. Mon idée était qu'il serait bien d'ajouter un IEnumeratorMT qui verrouillerait la collection pendant qu'un pour chaque se produise et ajouterait le code associé ...
Eric Ouellet

L'autre chose qui pourrait également se produire est que lorsque vous obtenez un itérateur, vous pouvez verrouiller la collection et lorsque votre itérateur est collecté par GC, vous pouvez déverrouiller la collection. Selon Microsfot, ils vérifient déjà si l'IEnumerable est également un IDisposable et appellent le GC si c'est le cas à la fin d'un ForEach. Le problème principal est qu'ils utilisent également IEnumerable ailleurs sans appeler le GC, vous ne pouvez donc pas vous fier à cela. Avoir une nouvelle interface MT claire pour IEnumerable activant le verrouillage résoudrait le problème, au moins une partie de celui-ci. (Cela n'empêcherait pas les gens de ne pas l'appeler).
Eric Ouellet

Il est très mauvais pour une GetEnumeratorméthode publique de laisser une collection verrouillée après son retour; de telles conceptions peuvent facilement conduire à une impasse. Le IEnumerable<T>n'indique pas si une énumération peut se terminer même si une collection est modifiée; le mieux que l'on puisse faire est d'écrire ses propres méthodes pour qu'elles le fassent, et d'avoir des méthodes qui acceptent de IEnumerable<T>documenter le fait qu'elles ne seront thread-safe que si elles IEnumerable<T>supportent l'énumération thread-safe.
supercat

Ce qui aurait été le plus utile aurait été d' IEnumerable<T>avoir inclus une méthode "Instantané" avec le type de retour IEnumerable<T>. Les collections immuables pourraient se restituer; une collection bornée pourrait, si rien d'autre ne se copiait sur un List<T>ou T[]et l'appelait GetEnumerator. Certaines collections illimitées pourraient être implémentées Snapshot, et celles qui ne le pourraient pas pourraient lever une exception sans essayer de remplir une liste avec leur contenu.
supercat

1

Dans l'exécution séquentielle de code, les structures de données utilisées sont différentes du code (bien écrit) exécutant simultanément. La raison en est que le code séquentiel implique un ordre implicite. Cependant, le code simultané n'implique aucune commande; mieux encore, cela implique l'absence d'un ordre défini!

Pour cette raison, les structures de données avec un ordre implicite (comme la liste) ne sont pas très utiles pour résoudre des problèmes simultanés. Une liste implique un ordre, mais elle ne définit pas clairement ce qu'est cet ordre. De ce fait, l'ordre d'exécution du code manipulant la liste déterminera (dans une certaine mesure) l'ordre implicite de la liste, qui est en conflit direct avec une solution concurrente efficace.

N'oubliez pas que la concurrence est un problème de données, pas un problème de code! Vous ne pouvez pas implémenter le code en premier (ou réécrire le code séquentiel existant) et obtenir une solution concurrente bien conçue. Vous devez d'abord concevoir les structures de données tout en gardant à l'esprit que l'ordre implicite n'existe pas dans un système simultané.


1

L'approche de copie et d'écriture sans verrou fonctionne très bien si vous ne traitez pas trop d'éléments. Voici une classe que j'ai écrite:

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

exemple d'utilisation: commandes_BUY = CopyAndWriteList.Clear (commandes_BUY);


au lieu de verrouiller, il crée une copie de la liste, modifie la liste et définit la référence à la nouvelle liste. Ainsi, tous les autres threads qui itèrent ne poseront aucun problème.
Rob The Quant

0

J'en ai implémenté un semblable à celui de Brian . Le mien est différent:

  • Je gère directement la baie.
  • Je n'entre pas les verrous dans le bloc d'essai.
  • J'utilise yield returnpour produire un énumérateur.
  • Je soutiens la récursivité des verrous. Cela permet des lectures à partir de la liste pendant l'itération.
  • J'utilise des verrous de lecture évolutifs lorsque cela est possible.
  • DoSyncet des GetSyncméthodes permettant des interactions séquentielles qui nécessitent un accès exclusif à la liste.

Le code :

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}

Que se passe-t-il si deux threads pénètrent au début du trybloc Removeou dans l'indexeur en même temps?
James

@James qui ne semble pas possible. Lisez les remarques sur msdn.microsoft.com/en-us/library/… . En exécutant
Ronnie Overby

@Ronny Overby: Intéressant. Compte tenu de cela, je soupçonne que cela fonctionnerait beaucoup mieux si vous supprimiez le UpgradableReadLock de toutes les fonctions où la seule opération effectuée dans le temps entre le verrou de lecture évolutif et le verrou d'écriture - la surcharge de prendre tout type de verrou est tellement plus que la vérification pour voir si le paramètre est hors de portée que le simple fait de faire cette vérification à l'intérieur du verrou d'écriture fonctionnerait probablement mieux.
James

Cette classe ne semble pas non plus très utile, car les fonctions basées sur l'offset (la plupart d'entre elles) ne peuvent jamais vraiment être utilisées en toute sécurité à moins qu'il y ait un schéma de verrouillage externe de toute façon, car la collection peut changer entre lorsque vous décidez où mettre ou obtenir quelque chose et quand vous l'avez réellement.
James

1
Je voulais dire officiellement que je reconnais que l'utilité de la IListsémantique dans des scénarios simultanés est au mieux limitée. J'ai probablement écrit ce code avant d'en arriver à cette réalisation. Mon expérience est la même que celle de l'auteur de la réponse acceptée: j'ai fait un essai avec ce que je savais sur la synchronisation et IList <T> et j'ai appris quelque chose en faisant cela.
Ronnie Overby
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.