J'étais ravi de voir le nouvel espace de noms System.Collections.Concurrent
dans .Net 4.0, plutôt sympa! J'ai vu ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
et BlockingCollection
.
Une chose qui semble manquer mystérieusement est un ConcurrentList<T>
. Dois-je écrire cela moi-même (ou l'obtenir du Web :))?
Est-ce que je manque quelque chose d'évident ici?
Je ai essayé il y a quelques temps (aussi: sur GitHub ). Ma mise en œuvre a eu quelques problèmes, que je ne vais pas aborder ici. Laissez-moi vous dire, plus important encore, ce que j'ai appris.
Tout d’abord, il n’ya aucun moyen d’obtenir une implémentation complète de IList<T>
qui soit sans verrou et thread-safe. En particulier, les insertions et suppressions aléatoires sont pas au travail, sauf si vous oubliez également l'accès aléatoire O(1) (c'est-à-dire, à moins que vous "trichiez" et n'utilisiez qu'une sorte de liste chaînée l'indexation sucer).
Ce que je pensais que pouvait valoir la peine, c’était un sous-ensemble limité de IList<T>
: en particulier, qui autoriserait une Add
et fournirait un accès aléatoire en lecture seule par index (mais sans Insert
, RemoveAt
, etc., et aussi aucun accès aléatoire écriture).
C'était l'objectif de ma ConcurrentList<T>
implementation . Mais lorsque j'ai testé ses performances dans des scénarios multithreads, j'ai constaté que simplement la synchronisation ajoute plus rapidement à un List<T>
était plus rapide. Fondamentalement, ajouter à 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'affecter à un élément d'un tableau; c'est vraiment ça). Vous auriez besoin de ton d'écrits simultanés pour voir toute sorte de conflit de verrouillage à ce sujet; et même dans ce cas, la performance moyenne de chaque écriture l'emporterait sur l'implémentation plus coûteuse, bien que sans verrouillage, dans ConcurrentList<T>
.
Dans le cas relativement rare où le tableau interne de la liste doit se redimensionner lui-même, vous ne payez qu'un faible coût. En conclusion, j’ai conclu qu’il s’agissait du scénario de niche one dans lequel un type de collection add-only ConcurrentList<T>
aurait un sens: lorsque vous souhaitez garantir une surcharge d’ajout d’un élément sur chaque appel ( donc, par opposition à un objectif de performance amorti).
Ce n'est tout simplement pas aussi utile qu'une classe qu'on pourrait le penser.
Pour quoi utiliseriez-vous une liste concurrente?
Le concept d'un conteneur d'accès aléatoire dans un monde threadé n'est pas aussi utile qu'il peut paraître. 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 y est. Les classes les plus courantes sont le ConcurrentBag et en particulier le BlockingCollection.
Avec tout le respect que je dois aux excellentes réponses déjà fournies, il arrive que je veuille simplement un IList thread-safe. Rien d'avancé ou de fantaisie. La performance est importante dans de nombreux cas, mais parfois, cela ne pose aucun problème. Oui, il y aura toujours des défis sans méthodes telles que "TryGetValue", etc., mais la plupart des cas, je veux simplement quelque chose que je puisse énumérer sans avoir à m'inquiéter de tout mettre en sécurité. Et oui, quelqu'un peut probablement trouver un "bogue" dans mon implémentation qui pourrait conduire à un blocage ou quelque chose (mais je suppose) mais soyons honnête: quand il s'agit de multi-threading, si vous n'écrivez pas votre code correctement, va dans l'impasse quand même. Dans cet esprit, j'ai décidé de faire une implémentation simple ConcurrentList qui répond à ces besoins de base.
Et pour ce qui en vaut la peine: j’ai fait un test de base en ajoutant 10 000 000 articles à la liste régulière et à la liste concurrente. Les résultats sont les suivants:
Liste terminée en: 7793 millisecondes . Concurrente terminée 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
}
ConcurrentList
(en tant que tableau redimensionnable, pas une liste chaînée) n'est pas facile à écrire avec des opérations non bloquantes. Son API ne traduit pas bien en une version "concurrente".
La raison pour laquelle il n’existe pas de liste ConcurrentList est qu’elle ne peut fondamentalement pas être écrite. La raison en est que plusieurs opérations importantes dans IList reposent sur des index et que cela ne fonctionnera tout simplement pas. Par exemple:
int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");
L’effet recherché par l’auteur consiste à 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’opération Insert
mettra réellement le "chien" après le chat, pas avant.
Les nombreuses implémentations que vous voyez proposées 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 ressemblant à une liste dans un environnement multithread, vous ne pouvez pas y arriver en mettant les méthodes d'implémentation de locks inside list. Vous devez vous assurer que tout index que vous utilisez réside entièrement dans le contexte du verrou. Le résultat est que vous pouvez utiliser une liste dans un environnement multithread avec le verrouillage correct, mais que la liste elle-même ne peut pas exister dans ce monde.
Si vous pensez avoir besoin d'une liste simultanée, il n'y a en réalité que deux possibilités:
Si vous avez un ConcurrentBag et êtes dans une position où vous devez le passer en tant qu'IList, vous avez un problème, car la méthode que vous appelez a spécifié qu'elle pourrait essayer de faire quelque chose comme je l'ai fait ci-dessus avec le chat et 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 soit le reformuler pour que ce soit le cas, ou, si vous ne pouvez pas, vous devrez le gérer très soigneusement. Vous devrez certainement créer votre propre collection avec ses propres verrous et appeler la méthode incriminée dans un verrou.
Dans les cas où les écritures sont beaucoup plus nombreuses que les écritures, ou (aussi fréquente qu'elles soient) sont non concurrentes, une approche copy-on-write peut être appropriée.
La mise en œuvre illustrée ci-dessous est
var snap = _list; snap[snap.Count - 1];
ne jettera jamais (eh bien, sauf pour une liste vide bien sûr), et vous obtenez également une énumération sécurisée pour les threads avec la sémantique de l'instantané gratuitement .. comment AMOUR 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. Quand vous voulez modifier, vous
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;
}
}
}
Utilisation
CopyOnWriteSwapper.Swap(ref _myList,
orig => new List<string>(orig),
clone => clone.Add("asdf"));
Si vous avez besoin de plus de performances, il sera utile de ne pas générer de méthode, par exemple. créez une méthode pour chaque type de modification (Ajouter, Supprimer, ...) de votre choix et codez en dur les pointeurs de fonction cloner
et op
.
N.B. # 1 Il est de votre responsabilité de vous assurer que personne ne modifie la structure de données (supposée) immuable. Nous ne pouvons rien faire dans une implémentation générique pour empêcher cela, mais en vous spécialisant dans List<T>
, vous pouvez vous protéger contre les modifications en utilisant List.AsReadOnly ()
N.B. # 2 Faites attention aux valeurs de la liste. L’approche de la copie en écriture située au-dessus protège uniquement l’appartenance à la liste, mais si vous ne placez pas de chaînes, mais d’autres objets mutables, vous devez prendre soin de la sécurité des threads (verrouillage, par exemple). Mais cela est orthogonal à cette solution et par ex. le verrouillage des valeurs mutables peut être facilement utilisé sans problèmes. Vous devez juste en être conscient.
N.B. # 3 Si votre structure de données est volumineuse et que vous la modifiez fréquemment, l'approche copie-tout-en-écriture peut s'avérer prohibitive à la fois en termes de consommation de mémoire et de coût de la copie en CPU. Dans ce cas, vous voudrez peut-être utiliser MS/ Collections immuables à la place.
System.Collections.Generic.List<t>
est déjà thread-safe pour plusieurs lecteurs. Essayer de le rendre thread-safe pour plusieurs auteurs n'aurait pas de sens. (Pour des raisons déjà mentionnées par Henk et Stephen)
Certaines personnes ont souligné certains points essentiels (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 est que Microsoft doit apporter des modifications en profondeur au "foreach" pour faciliter l'utilisation de la collection MultiThreaded. En outre, il doit suivre ses propres règles d'utilisation d'IEnumerator. En attendant, nous pouvons écrire facilement une liste MultiThreadList qui utiliserait un itérateur bloquant, mais ne suivra pas "IList". Au lieu de cela, vous devrez définir votre propre interface "IListPersonnal" qui pourrait échouer avec "insert", "remove" et l'accesseur aléatoire (indexeur) sans exception. Mais qui voudra l'utiliser s'il n'est pas standard?
l’approche de copie et d’écriture sans verrou fonctionne très bien si vous ne traitez pas trop d’articles. Voici un cours que j'ai écrit:
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: orders_BUY = CopyAndWriteList.Clear (orders_BUY);
Dans l’exécution séquentielle du code, les structures de données utilisées sont différentes de celles exécutées simultanément (bien écrit). La raison en est que le code séquentiel implique un ordre implicite. Le code concurrent n'implique cependant aucun ordre; mieux encore, cela implique l'absence d'un ordre défini!
De ce fait, les structures de données avec un ordre implicite (comme List) ne sont pas très utiles pour résoudre des problèmes concurrents. Une liste implique un ordre, mais elle ne définit pas clairement en quoi consiste 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 entre directement en conflit avec une solution concurrente efficace.
Rappelez-vous que la concurrence est un problème de données, pas un problème de code! Vous ne pouvez pas d'abord mettre en œuvre le code (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é.
J'ai mis en place un semblable à Brian's . Le mien est différent:
yield return
pour produire un énumérateur.DoSync
et GetSync
permettant des interactions séquentielles nécessitant 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();
}
}