Quand utiliser une collection thread-safe

.NET Framework 4 propose cinq nouveaux types de collection spécialement conçus pour prendre en charge les opérations d’ajout et de suppression multithread. Pour assurer la sécurité des threads, ces types utilisent différents types de mécanismes de verrouillage efficace et de synchronisation sans verrou. La synchronisation ajoute une surcharge à une opération. La surcharge dépend du type de synchronisation utilisé, du type d’opérations effectuées et d’autres facteurs tels que le nombre de threads qui tentent d’accéder simultanément à la collection.

Dans certains scénarios, la surcharge de synchronisation est négligeable et permet au type multithread de s’exécuter beaucoup plus rapidement et d’évoluer beaucoup mieux que son équivalent qui n’est pas thread-safe quand il est protégé par un verrou externe. Dans d’autres scénarios, la surcharge peut entraîner une exécution et une scalabilité du type thread-safe à peu près identiques, ou même plus lentes, que celles de la version du type qui n’est pas thread-safe et qui est verrouillée de manière externe.

Les sections suivantes fournissent des conseils généraux sur l’utilisation d’une collection thread-safe par rapport à son équivalent non thread-safe doté d’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, les conseils ne sont pas spécifiques et ne sont pas nécessairement valides 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 des configurations et des charges d’ordinateur représentatives. Ce document utilise les termes suivants :

Scénario de producteur-consommateur pur
Tout thread donné ajoute ou supprime des éléments, mais pas les deux.

Scénario de producteur-consommateur mixte
Tout thread donné ajoute et supprime des éléments.

Speedup
Performances algorithmiques plus rapides par rapport à un autre type dans le même scénario.

Scalabilité
Augmentation des performances proportionnelles au nombre de cœurs sur l’ordinateur. Un algorithme qui évolue fonctionne plus rapidement sur huit cœurs que sur deux cœurs.

ConcurrentQueue(T) vs. Queue(T)

Dans les scénarios purs de producteur-consommateur, où le temps de traitement de chaque élément est très petit (quelques instructions), alors System.Collections.Concurrent.ConcurrentQueue<T> peut offrir des avantages modestes en matière de performances par rapport à un System.Collections.Generic.Queue<T> qui a un verrou externe. Dans ce scénario, ConcurrentQueue<T> fonctionne mieux quand un thread dédié effectue la mise en file d’attente et qu’un autre thread dédié annule la mise en file d’attente. Si vous n’appliquez pas cette règle, Queue<T> cela peut même être légèrement plus rapide que ConcurrentQueue<T> sur les ordinateurs qui ont plusieurs cœurs.

Lorsque le temps de traitement est d’environ 500 FLOPS (opérations à virgule flottante) ou plus, la règle à deux threads ne s’applique pas à ConcurrentQueue<T>, ce qui lui confère ensuite une très bonne extensibilité. Queue<T> ne s'adapte pas bien à ce scénario.

Dans les scénarios producteur-consommateur mixtes, quand le temps de traitement est très court, un Queue<T> qui a un externe verrou évolue mieux que ConcurrentQueue<T>. Toutefois, lorsque le temps de traitement est d’environ 500 FLOPS ou plus, alors ConcurrentQueue<T> s'adapte mieux.

ConcurrentStack par rapport à Stack

Dans les scénarios producteur-consommateur purs, quand 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 de type push dédié et un thread d’exécution de type pop dédié. Toutefois, à mesure que le nombre de threads augmente, les deux types ralentissent en raison d’une contention accrue et Stack<T> peuvent s’exécuter mieux que ConcurrentStack<T>. Lorsque le temps de traitement est d’environ 500 FLOPS ou plus, les deux types évoluent à un rythme similaire.

Dans les scénarios de producteur-consommateur mixtes, ConcurrentStack<T> est plus rapide pour les charges de travail petites et volumineuses.

L'utilisation de PushRange et TryPopRange peut accélérer considérablement les temps d'accès.

ConcurrentDictionary et dictionnaire

En règle générale, utilisez un System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> dans tout scénario où vous ajoutez et mettez à jour des clés ou des valeurs de manière simultanée à partir de plusieurs threads. Dans les scénarios qui impliquent des mises à jour fréquentes et relativement peu de lectures, 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, il ConcurrentDictionary<TKey,TValue> est généralement beaucoup plus rapide sur les ordinateurs qui ont un nombre quelconque de cœurs.

Dans les scénarios qui impliquent des mises à jour fréquentes, vous pouvez augmenter le degré d’accès concurrentiel dans 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, autant que possible, les opérations globales.

Si vous lisez uniquement la clé ou les valeurs, la Dictionary<TKey,TValue> synchronisation 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 collections simultanées.

Dans les scénarios de producteur-consommateur mixtes, ConcurrentBag<T> il est généralement beaucoup plus rapide et plus évolutif que n’importe quel autre type de collection simultané pour les charges de travail volumineuses et petites.

BlockingCollection

Lorsque la sémantique englobante et bloquante est requise, System.Collections.Concurrent.BlockingCollection<T> elle s’exécute probablement plus rapidement que n’importe quelle implémentation personnalisée. Il prend également en charge la gestion complète des annulations, des énumérations et des exceptions.

Voir aussi