210 votes

HashSet<T> concourant dans .NET Framework ?

J'ai la classe suivante.

class Test{
    public HashSet<string> Data = new HashSet<string>();
}

J'ai besoin de modifier le champ "Data" à partir de différents threads, et j'aimerais donc avoir des avis sur mon implémentation thread-safe actuelle.

class Test{
    public HashSet<string> Data = new HashSet<string>();

    public void Add(string Val){
            lock(Data) Data.Add(Val);
    }

    public void Remove(string Val){
            lock(Data) Data.Remove(Val);
    }
}

Existe-t-il une meilleure solution, qui consisterait à passer directement au champ et à le protéger contre l'accès simultané de plusieurs threads ?

2voto

taffer Points 1433

Les solutions basées sur ConcurrentDictionary<TKey, TValue> sont généralement bien modulables ; cependant, si vous devez accéder à la base de données des Count , Keys ou Values ou si vous itérez à travers les éléments, cela devient pire qu'une collection à verrouillage unique. C'est parce que ConcurrentDictionary utilise un groupe de verrous (par défaut, leur nombre dépend du nombre de cœurs du processeur) et l'accès à ces membres entraîne l'acquisition de tous les verrous, de sorte que les performances sont d'autant plus mauvaises que le nombre de cœurs du processeur est élevé.

Une autre réponse suggère d'utiliser des collections immuables. Bien qu'elles soient sûres pour les threads, elles ne sont performantes que si vous y ajoutez rarement de nouveaux éléments (ce qui crée toujours une nouvelle instance, tout en essayant d'hériter du plus grand nombre possible de nœuds de l'instance précédente), mais même dans ce cas, elles sont généralement moins performantes.

Je me suis retrouvé avec une autre solution (que j'ai également appliqué à mon ThreadSafeHashSet<T> plus tard) : par opposition à ConcurrentDictionary Je n'utilise qu'un seul verrou, mais seulement temporairement : de temps en temps, les nouveaux éléments sont déplacés vers un espace de stockage totalement dépourvu de verrou, où même le retrait et la réinsertion des mêmes clés se font sans verrou, ce qui les rend très rapides. Aucune temporisation n'est utilisée pour effectuer ces fusions. Une fusion vers la mémoire sans verrou n'a lieu que lorsque la mémoire avec verrou doit être consultée et qu'elle a été créée il y a "suffisamment longtemps", c'est-à-dire configurable .

Remarque : Voir le tableau de comparaison des performances à l'adresse suivante Remarques de la section ThreadSafeDictionary<TKey TValue> (elle a les mêmes caractéristiques que la classe ThreadSafeHashSet<T> ) pour voir s'il s'agit d'un bon choix pour vos besoins. Aquí vous pouvez trouver la source des tests de performance si vous souhaitez les exécuter vous-même.

La source est disponible aquí et vous pouvez également le télécharger en tant que Paquet NuGet .

1voto

notgiven Points 29

J'ai constaté que ni le simple verrouillage des méthodes d'ajout et de retrait d'un System.Collections.Generic.HashSet, ni l'encapsulation du ConcurrentDictionary du framework ne sont suffisants dans les scénarios à "haut débit" qui exigent de bonnes performances.

Cette idée simple permet déjà d'obtenir d'assez bonnes performances :

public class ExampleHashSet<T>
{
    const int ConcurrencyLevel = 124;
    const int Lower31BitMask = 0x7FFFFFFF;

    HashSet<T>[] sets = new HashSet<T>[ConcurrencyLevel];
    IEqualityComparer<T> comparer;

    public ExampleHashSet()
    {
        comparer = EqualityComparer<T>.Default;

        for(int i = 0; i < ConcurrencyLevel; i++)
            sets[i] = new HashSet<T>();
    }

    public bool Add(T item)
    {
        int hash = (comparer.GetHashCode(item) & Lower31BitMask) % ConcurrencyLevel;

        lock(sets[hash]) 
        {
            return sets[hash].Add(item);
        }
    }

    public bool Remove(T item)
    {
        int hash = (comparer.GetHashCode(item) & Lower31BitMask) % ConcurrencyLevel;

        lock(sets[hash]) 
        {
            return sets[hash].Remove(item);
        }
    }

    // further methods ...
}

Le HashSet du système est enveloppé, mais contrairement à d'autres réponses, nous maintenons des verrous sur plusieurs HashSets. Différents threads peuvent "travailler" sur différents HashSets, ce qui réduit le temps d'attente global.

Cette idée peut être généralisée et mise en œuvre directement dans le HashSet lui-même (en maintenant des verrous sur les buckets, au lieu de verrouiller des les ensembles complets). Un exemple peut être trouvé aquí .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X