Quand utiliser une collection thread-safe
Le .NET Framework version 4 présente cinq nouveaux types de collection conçus spécialement pour prendre en charge les opérations d'ajout et de suppression multithread. Pour garantir la sécurité des threads, ces nouveaux types utilisent différents genres de mécanismes de verrouillage et de synchronisation sans verrou efficaces. La synchronisation ajoute de la charge mémoire à une opération. La quantité de charge mémoire dépend du genre de synchronisation utilisé, du type d'opérations exécuté et d'autres facteurs tels que le nombre de threads qui essaient d'accéder à la collection simultanément.
Dans certains scénarios, la charge mémoire de synchronisation est négligeable et permet au type multithread de s'exécuter considérablement plus rapidement et d'évoluer nettement mieux que son équivalent non thread-safe en cas de protection par un verrou externe. Dans d'autres scénarios, la charge mémoire peut générer une exécution et une évolutivité du type thread-safe égales ou plus lentes que la version non thread-safe et verrouillée extérieurement du type.
Les sections suivantes fournissent des recommandations générales concernant le moment où utiliser une collection thread-safe ou son équivalent non thread-safe possédant un verrou fourni par l'utilisateur autour de ses opérations de lecture et d'écriture. Étant donné que les performances peuvent varier en fonction de nombreux facteurs, ces recommandations ne sont pas spécifiques et ne sont pas nécessairement valables dans toutes les circonstances. Si les performances sont très importantes, la meilleure façon de déterminer le type de collection à utiliser consiste à mesurer les performances en fonction de configurations et de charges d'ordinateur représentatives. Ce document utilise les conditions suivantes :
Scénario producteur-consommateur pur
Tout thread donné ajoute ou supprime des éléments, mais pas les deux.Scénario producteur-consommateur mixte
Tout thread donné ajoute et supprime des éléments.Accélération
Performances algorithmiques plus rapides par rapport à un autre type dans le même scénario.Évolutivité
Augmentation des performances proportionnelle au nombre de cœurs de l'ordinateur. Un algorithme évolutif s'exécute plus vite sur huit cœurs qu'il ne le fait sur deux cœurs.
ConcurrentQueue (T) etQueue(T)
Dans les scénarios producteur-consommateur purs, où le temps de traitement de chaque élément est très court (quelques instructions), System.Collections.Concurrent.ConcurrentQueue<T> peut offrir des avantages modestes en matière de performances par rapport à un System.Collections.Generic.Queue<T> avec un verrou externe. Dans ce scénario, ConcurrentQueue<T> fonctionne mieux lorsqu'un thread dédié effectue la mise en file d'attente et un autre thread dédié annule la mise en file d'attente. Si vous n'appliquez pas cette règle, Queue<T> peut même s'exécuter légèrement plus rapidement que ConcurrentQueue<T> sur des ordinateurs à plusieurs cœurs.
Lorsque le temps de traitement est autour de 500 opérations en virgule flottante ou plus, la règle de deux threads ne s'applique pas à ConcurrentQueue<T>, qui possède alors une très bonne évolutivité. Queue<T> n'évolue pas bien dans ce scénario.
Dans les scénarios producteur-consommateur mixtes, lorsque le temps de traitement est très court, une Queue<T> qui possède un verrou externe évolue mieux que ConcurrentQueue<T>. Toutefois, lorsque le temps de traitement est autour de 500 opérations en virgule flottante ou plus, la ConcurrentQueue<T> évolue mieux.
ConcurrentStack etStack
Dans les scénarios producteur-consommateur purs, lorsque le temps de traitement est très court, System.Collections.Concurrent.ConcurrentStack<T> et System.Collections.Generic.Stack<T> qui a un verrou externe s'exécuteront probablement de la même manière avec un thread d'exécution push dédié et un thread dépilant dédié. Toutefois, à mesure que le nombre de threads augmente, les deux types ralentissent à cause de l'augmentation des conflits et Stack<T> peut fonctionner mieux que ConcurrentStack<T>. Lorsque le temps de traitement est autour de 500 opérations en virgule flottante ou plus, les deux types évoluent environ au même taux.
Dans les scénarios producteur-consommateur mixtes, ConcurrentStack<T> est plus rapide à la fois pour les petites charges de travail et les grandes.
L'utilisation de PushRange et TryPopRangepeut accélérer considérablement les temps d'accès.
ConcurrentDictionary etDictionary
En général, vous utilisez un System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> dans tout scénario où vous ajoutez et mettez à jour des clés ou des valeurs simultanément à partir de plusieurs threads. Dans les scénarios qui impliquent des mises à jour fréquentes et des lectures relativement peu nombreuses, le ConcurrentDictionary<TKey, TValue> offre généralement des avantages modestes. Dans les scénarios qui impliquent de nombreuses lectures et de nombreuses mises à jour, le ConcurrentDictionary<TKey, TValue> est généralement considérablement plus rapide, quel que soit le nombre de cœurs des ordinateurs.
Dans les scénarios qui impliquent des mises à jour fréquentes, vous pouvez augmenter le degré d'accès concurrentiel dans le ConcurrentDictionary<TKey, TValue>, puis mesurer pour voir si les performances augmentent sur les ordinateurs qui ont plus de cœurs. Si vous modifiez le niveau d'accès concurrentiel, évitez les opérations globales autant que possible.
Si vous lisez uniquement des clé ou des valeurs, le Dictionary<TKey, TValue> est plus rapide car aucune synchronisation n'est requise si le dictionnaire n'est pas modifié par des threads.
ConcurrentBag
Dans les scénarios producteur-consommateur purs, System.Collections.Concurrent.ConcurrentBag<T> s'exécutera probablement plus lentement que les autres types de collection simultanés.
Dans les scénarios producteur-consommateur mixtes, ConcurrentBag<T> est généralement beaucoup plus rapide et plus évolutif que les autres types de collection simultanés à la fois pour les petites charges de travail et pour les grandes.
BlockingCollection
Lorsqu'une sémantique de liaison et de blocage est requise, System.Collections.Concurrent.BlockingCollection<T> s'exécutera probablement plus rapidement que toute implémentation personnalisée. Il prend également en charge une gestion riche des annulations, énumérations et exceptions.