web-dev-qa-db-fra.com

Comment créer une liste générique thread-safe?

J'ai une liste générique ci-dessous

public static readonly List<Customer> Customers = new List<Customer>();

J'utilise les méthodes ci-dessous pour cela:

.Add
.Find
.FirstOrDefault

Les 2 derniers sont des extensions LINQ.

J'aurais besoin de rendre ce thread-safe pour pouvoir exécuter plusieurs instances de la classe conteneur.

Comment y parvenir?

22
The Light

Si ce sont les seules fonctions que vous utilisez sur List<T>, le moyen le plus simple est d'écrire un wrapper rapide qui synchronise l'accès avec un lock

class MyList<T> { 
  private List<T> _list = new List<T>();
  private object _sync = new object();
  public void Add(T value) {
    lock (_sync) {
      _list.Add(value);
    }
  }
  public bool Find(Predicate<T> predicate) {
    lock (_sync) {
      return _list.Find(predicate);
    }
  }
  public T FirstOrDefault() {
    lock (_sync) {
      return _list.FirstOrDefault();
    }
  }
}

Je recommande fortement l'approche d'un nouveau type + objet de verrouillage privé. Cela rend beaucoup plus évident pour l’intéressé qui hérite de votre code ce que l’intention réelle était.

Notez également que .Net 4.0 a introduit un nouvel ensemble de collections spécifiquement destinées à être utilisées à partir de plusieurs threads. Si l’un de ceux-ci répond à vos besoins, je vous recommande vivement de l’utiliser plutôt que de rouler vous-même. 

  • ConcurrentStack<T>
  • ConcurrentQueue<T>
42
JaredPar

Si vous utilisez la version 4 ou supérieure du framework .NET, vous pouvez utiliser les collections thread-safe .

Vous pouvez remplacer List<T> par ConcurrentBag<T>:

namespace Playground.Sandbox
{
    using System.Collections.Concurrent;
    using System.Threading.Tasks;

    public static class Program
    {
        public static void Main()
        {
            var items = new[] { "Foo", "Bar", "Baz" };
            var bag = new ConcurrentBag<string>();
            Parallel.ForEach(items, bag.Add);
        }
    }
}
8
Albireo

Pour développer la réponse de @ JaradPar, voici une implémentation complète avec quelques fonctionnalités supplémentaires, comme décrit dans le résumé.

    /// <summary>
/// a thread-safe list with support for:
/// 1) negative indexes (read from end).  "myList[-1]" gets the last value
/// 2) modification while enumerating: enumerates a copy of the collection.
/// </summary>
/// <typeparam name="TValue"></typeparam>
public class ConcurrentList<TValue> : IList<TValue>
{
    private object _lock = new object();
    private List<TValue> _storage = new List<TValue>();
    /// <summary>
    /// support for negative indexes (read from end).  "myList[-1]" gets the last value
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public TValue this[int index]
    {
        get
        {
            lock (_lock)
            {
                if (index < 0)
                {
                    index = this.Count - index;
                }
                return _storage[index];
            }
        }
        set
        {
            lock (_lock)
            {
                if (index < 0)
                {
                    index = this.Count - index;
                }
                _storage[index] = value;
            }
        }
    }

    public void Sort()
    {
        lock (_lock)
        {
            _storage.Sort();
        }
    }

    public int Count
    {
        get
        {
            return _storage.Count;
        }
    }

    bool ICollection<TValue>.IsReadOnly
    {
        get
        {
            return ((IList<TValue>)_storage).IsReadOnly;
        }
    }

    public void Add(TValue item)
    {
        lock (_lock)
        {
            _storage.Add(item);
        }
    }

    public void Clear()
    {
        lock (_lock)
        {
            _storage.Clear();
        }
    }

    public bool Contains(TValue item)
    {
        lock (_lock)
        {
            return _storage.Contains(item);
        }
    }

    public void CopyTo(TValue[] array, int arrayIndex)
    {
        lock (_lock)
        {
            _storage.CopyTo(array, arrayIndex);
        }
    }


    public int IndexOf(TValue item)
    {
        lock (_lock)
        {
            return _storage.IndexOf(item);
        }
    }

    public void Insert(int index, TValue item)
    {
        lock (_lock)
        {
            _storage.Insert(index, item);
        }
    }

    public bool Remove(TValue item)
    {
        lock (_lock)
        {
            return _storage.Remove(item);
        }
    }

    public void RemoveAt(int index)
    {
        lock (_lock)
        {
            _storage.RemoveAt(index);
        }
    }

    public IEnumerator<TValue> GetEnumerator()
    {

        lock (_lock)
        {
            lock (_lock)
            {
                return (IEnumerator<TValue>)_storage.ToArray().GetEnumerator();
            }
        }
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
7
JasonS

Vous devrez utiliser des verrous à chaque endroit où la collection est modifiée ou itérée.

Ou bien utilisez l'une des nouvelles structures de données thread-safe, comme ConcurrentBag .

2
Tudor

Utilisez le mot clé lock lorsque vous manipulez la collection, c.-à-d. Votre ajout/recherche:

lock(Customers) {
    Customers.Add(new Customer());
}
1
Darren

Ne jamais utiliser ConcurrangBag pour les données commandées. Utilisez Array à la place

0
Moctar Haiz

Rendre votre action accessible par un seul en utilisant le verrou sur tout objet privé

Voir: Classe de file d'attente générique Thread Safe

http://www.codeproject.com/Articles/38908/Thread-Safe-Generic-Queue-Class

0
JSJ

Ok, j'ai donc dû réécrire complètement ma réponse. Après 2 jours de test, je dois dire que le code de JasonS a quelques défauts, je suppose à cause des énumérateurs. Alors que l'un des threads utilise foreach et que l'autre modifie la liste, il renvoie des exceptions.

J'ai donc trouvé cette réponse , et cela fonctionne bien pour moi les 48 dernières heures non-stop, je suppose que plus de 100 000 threads ont été créés dans mon application et utilisés.

La seule chose que j'ai changée - je me suis déplacé en entrant dans les serrures en dehors de la section try-finally. Lisez ici sur les exceptions possibles. En outre, si vous lisez MSDN, ils ont la même approche.

Mais, comme mentionné dans le lien ci-dessous, List ne peut pas être thread-safe à 100%, ce qui explique probablement pourquoi il n'y a pas d'implémentation ConcurentList par défaut en c #.

0
Max