web-dev-qa-db-fra.com

Pas de liste concurrente <T> dans .Net 4.0?

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?

181
Alan

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.

156
Dan Tao

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.

33
Henk Holterman

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
}
18
Brian Booth

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".

11
Stephen Cleary

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:

  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 ê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.

8
Steve Benz

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

  • sans verrou
  • extrêmement rapide pour les lectures simultanées, même si des modifications simultanées sont en cours, quel que soit le temps qu'elles prennent
  • parce que les "instantanés" sont immuables, lockless atomicity est possible, c’est-à-dire que 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é!
  • implémenté de manière générique, applicable à toute structure de données et à tout type de modification
  • dead 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. Quand vous voulez modifier, vous

  1. cloner la structure
  2. faire des modifications sur le clone
  3. permutation atomique 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;
        }
    }
}

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.

5
Eugene Beresovsky

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)

3
Billy ONeal

Certaines personnes ont souligné certains points essentiels (et certaines de mes pensées):

  • Cela pourrait sembler insensé ou impossible à l’accès aléatoire (indexeur), mais cela me semble bien. Il suffit de penser qu'il existe de nombreuses méthodes sur les collections multithreads qui pourraient échouer, comme Indexer et Delete. Vous pouvez également définir une action d'échec (de secours) pour l'accesseur en écriture, telle que "échec" ou simplement "ajouter à la fin". 
  • Ce n'est pas parce qu'il s'agit d'une collection multithread qu'il sera toujours utilisé dans un contexte multithread. Ou il pourrait également être utilisé par un seul auteur et un seul lecteur.
  • Une autre façon de pouvoir utiliser l'indexeur de manière sécurisée pourrait consister à envelopper des actions dans un verrou de la collection à l'aide de sa racine (si elle est rendue publique).
  • Pour beaucoup de gens, rendre un rootLock visible devient une "bonne pratique". Je ne suis pas sûr à 100% de ce point car s'il est masqué, l'utilisateur perd beaucoup de flexibilité. Nous devons toujours nous rappeler que la programmation multithread n’est destinée à personne. Nous ne pouvons empêcher tous les types d’utilisation abusive.
  • Microsoft devra travailler et définir un nouveau standard pour introduire le bon usage de la collection multithread. Tout d'abord, IEnumerator ne doit pas avoir de moveNext mais doit avoir un GetNext qui renvoie true ou false et obtient un paramètre out de type T (de cette manière, l'itération ne serait plus bloquée). De plus, Microsoft utilise déjà "using" en interne dans le foreach mais utilise parfois directement IEnumerator sans l'envelopper avec "using" (un bogue dans la vue de collection et probablement à plusieurs endroits). Ce bogue supprime tout bon potentiel pour un 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 pour chaque méthode. 

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?

2
Eric Ouellet

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

1
Rob The Quant

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é.

0
Blueprint41

J'ai mis en place un semblable à Brian's . Le mien est différent:

  • Je gère le tableau directement.
  • Je n'entre pas dans les serrures du bloc try.
  • J'utilise yield return pour produire un énumérateur.
  • Je soutiens la récursion de verrouillage. Cela permet de lire depuis la liste pendant l'itération.
  • J'utilise des verrous de lecture évolutifs dans la mesure du possible.
  • Méthodes 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();
    }
}
0
Ronnie Overby