J'étais ravi de voir le nouvel System.Collections.Concurrent
espace de noms dans .Net 4.0, plutôt sympa! Je l' ai vu ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
et 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?
Réponses:
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 à unAdd
et de fournir au hasard en lecture seule l' accès par index (mais nonInsert
,RemoveAt
etc., 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 à aList<T>
était plus rapide . Fondamentalement, l'ajout à unList<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ûteuseConcurrentList<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.
la source
List<T>
celui qui utilise une synchronisation basée sur un ancien moniteur, il y en aSynchronizedCollection<T>
dans la BCL: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
serait 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).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.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
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.
la source
Monitor
pour le faire de toute façon, il n'y a aucune raison pour une liste simultanée.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.
la source
RemoveAt(int index)
n'est jamaisInsert(int index, T item)
sûr pour les threads, n'est sûr que pour l'index == 0, le retour deIndexOf()
est immédiatement obsolète, etc. Ne commencez même pas à propos dethis[int]
.ReaderWriterLockSlim
peut être bloqué facilement en utilisantEnterUpgradeableReadLock()
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.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, commeConcurrentDictionary
lesAddOrGet
etc. NB Votre répétition constante (et redondante car les membres sont déjà marqués comme tels par le trait de soulignement) desthis.
encombrements.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".la source
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:
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'Insert
opé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:
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.
la source
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
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é!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
Code
Usage
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
cloner
etop
.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 .
la source
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)la source
Certaines personnes ont signalé certains points positifs (et certaines de mes pensées):
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?
la source
ConcurrentOrderedBag<T>
qui inclurait une implémentation en lecture seule deIList<T>
, mais offrirait également uneint Add(T value)
méthode entièrement thread-safe . Je ne vois pas pourquoi desForEach
changements seraient nécessaires. Bien que Microsoft ne le dise pas explicitement, leur pratique suggère qu'il est parfaitement acceptableIEnumerator<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.GetEnumerator
méthode publique de laisser une collection verrouillée après son retour; de telles conceptions peuvent facilement conduire à une impasse. LeIEnumerable<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 deIEnumerable<T>
documenter le fait qu'elles ne seront thread-safe que si ellesIEnumerable<T>
supportent l'énumération thread-safe.IEnumerable<T>
avoir inclus une méthode "Instantané" avec le type de retourIEnumerable<T>
. Les collections immuables pourraient se restituer; une collection bornée pourrait, si rien d'autre ne se copiait sur unList<T>
ouT[]
et l'appelaitGetEnumerator
. Certaines collections illimitées pourraient être implémentéesSnapshot
, et celles qui ne le pourraient pas pourraient lever une exception sans essayer de remplir une liste avec leur contenu.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é.
la source
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:
exemple d'utilisation: commandes_BUY = CopyAndWriteList.Clear (commandes_BUY);
la source
J'en ai implémenté un semblable à celui de Brian . Le mien est différent:
yield return
pour produire un énumérateur.DoSync
et desGetSync
méthodes permettant des interactions séquentielles qui nécessitent un accès exclusif à la liste.Le code :
la source
try
blocRemove
ou dans l'indexeur en même temps?IList
sé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.