.NET - Verrouillage de dictionnaire et ConcurrentDictionary

131

Je n'ai pas trouvé suffisamment d'informations sur ConcurrentDictionary types, alors j'ai pensé que je poserais des questions à ce sujet ici.

Actuellement, j'utilise a Dictionarypour contenir tous les utilisateurs auxquels accède en permanence par plusieurs threads (à partir d'un pool de threads, donc pas de quantité exacte de threads), et il a un accès synchronisé.

J'ai récemment découvert qu'il y avait un ensemble de collections thread-safe dans .NET 4.0, et cela semble très agréable. Je me demandais quelle serait l'option `` plus efficace et plus facile à gérer '', car j'ai le choix entre avoir un Dictionaryaccès normal avec un accès synchronisé ou avoir unConcurrentDictionary qui est déjà thread-safe.

Référence à .NET 4.0 ConcurrentDictionary

TheAJ
la source
3
Vous voudrez peut-être voir cet article de projet de code sur le sujet
nawfal

Réponses:

146

Une collection thread-safe par rapport à une collection non threadsafe peut être considérée différemment.

Envisagez un magasin sans commis, sauf à la caisse. Vous avez une tonne de problèmes si les gens n'agissent pas de manière responsable. Par exemple, disons qu'un client prend une canette d'une pyramide pendant qu'un employé est en train de construire la pyramide, tout l'enfer se déchaînerait. Ou, que se passe-t-il si deux clients atteignent le même article en même temps, qui gagne? Y aura-t-il un combat? Il s'agit d'une collection non threadsafe. Il existe de nombreuses façons d'éviter les problèmes, mais elles nécessitent toutes une sorte de verrouillage, ou plutôt un accès explicite d'une manière ou d'une autre.

D'un autre côté, pensez à un magasin avec un commis à un bureau, et vous ne pouvez faire vos achats que par lui. Vous faites la queue et lui demandez un article, il vous le ramène et vous sortez de la file. Si vous avez besoin de plusieurs articles, vous ne pouvez ramasser qu'autant d'articles à chaque aller-retour que vous vous en souvenez, mais vous devez faire attention à ne pas monopoliser le commis, cela mettra en colère les autres clients en ligne derrière vous.

Considérez ceci maintenant. Dans le magasin avec un employé, que se passe-t-il si vous vous rendez jusqu'au bout de la file et demandez au vendeur «Avez-vous du papier toilette», et il dit «Oui», puis vous dites «Ok, je» Je vous recontacterai quand je saurai combien j'ai besoin », puis au moment où vous serez de retour en tête de file, le magasin pourra bien sûr être épuisé. Ce scénario n'est pas empêché par une collection threadsafe.

Une collection threadsafe garantit que ses structures de données internes sont valides à tout moment, même si elles sont accessibles à partir de plusieurs threads.

Une collection non threadsafe n'offre pas de telles garanties. Par exemple, si vous ajoutez quelque chose à un arbre binaire sur un thread, alors qu'un autre thread est occupé à rééquilibrer l'arbre, il n'y a aucune garantie que l'élément sera ajouté, ou même que l'arbre sera toujours valide par la suite, il pourrait être corrompu au-delà de tout espoir.

Une collection threadsafe ne garantit cependant pas que les opérations séquentielles sur le thread fonctionnent toutes sur le même «instantané» de sa structure de données interne, ce qui signifie que si vous avez un code comme celui-ci:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

vous pourriez obtenir une NullReferenceException car entre tree.Countet tree.First(), un autre thread a effacé les nœuds restants dans l'arborescence, ce qui signifie First()que vous retournereznull .

Pour ce scénario, vous devez soit voir si la collection en question dispose d'un moyen sûr d'obtenir ce que vous voulez, soit vous devez peut-être réécrire le code ci-dessus, soit vous devrez peut-être verrouiller.

Lasse V. Karlsen
la source
9
Chaque fois que je lis cet article, même si tout est vrai, nous nous engageons d'une manière ou d'une autre sur la mauvaise voie.
scope_creep
19
Le principal problème dans une application compatible avec les threads est la modification des structures de données. Si vous pouvez éviter cela, vous vous épargnerez des tas d'ennuis.
Lasse V.Karlsen
89
C'est une belle explication de la sécurité des threads, mais je ne peux pas m'empêcher de penser que cela ne répond pas réellement à la question du PO. Ce que l'OP a demandé (et ce que je suis ensuite tombé sur cette question) était la différence entre l'utilisation d'une norme Dictionaryet la gestion du verrouillage vous-même par rapport à l'utilisation du ConcurrentDictionarytype intégré à .NET 4+. Je suis en fait un peu déconcerté que cela ait été accepté.
mclark1129
4
La sécurité des threads ne se limite pas à utiliser la bonne collection. Utiliser la bonne collection est un début, pas la seule chose à laquelle vous devez faire face. Je suppose que c'est ce que le PO voulait savoir. Je ne peux pas deviner pourquoi cela a été accepté, cela fait un moment que j'ai écrit ceci.
Lasse V. Karlsen
2
Je pense que ce que @ LasseV.Karlsen essaie de dire, c'est que ... la sécurité des threads est facile à réaliser, mais vous devez fournir un niveau de concurrence dans la façon dont vous gérez cette sécurité. Posez-vous la question suivante: "Puis-je effectuer plus d'opérations en même temps tout en gardant l'objet thread-safe?" Prenons cet exemple: deux clients veulent retourner des articles différents. Lorsque vous, en tant que responsable, faites le voyage pour réapprovisionner votre magasin, ne pouvez-vous pas simplement réapprovisionner les deux articles en un seul voyage et toujours gérer les demandes des deux clients, ou allez-vous faire un voyage chaque fois qu'un client retourne un article?
Alexandru le
68

Vous devez toujours être très prudent lorsque vous utilisez des collections thread-safe car cela ne signifie pas que vous pouvez ignorer tous les problèmes de threading. Lorsqu'une collection se présente comme thread-safe, cela signifie généralement qu'elle reste dans un état cohérent même lorsque plusieurs threads lisent et écrivent simultanément. Mais cela ne signifie pas qu'un seul thread verra une séquence "logique" de résultats s'il appelle plusieurs méthodes.

Par exemple, si vous vérifiez d'abord si une clé existe et que vous obtenez ensuite la valeur qui correspond à la clé, cette clé peut ne plus exister même avec une version ConcurrentDictionary (car un autre thread aurait pu supprimer la clé). Vous devez toujours utiliser le verrouillage dans ce cas (ou mieux: combiner les deux appels en utilisant TryGetValue ).

Alors utilisez-les, mais ne pensez pas que cela vous donne un laissez-passer gratuit pour ignorer tous les problèmes de concurrence. Vous devez toujours faire attention.

Mark Byers
la source
4
Donc, fondamentalement, vous dites que si vous n'utilisez pas correctement l'objet, cela ne fonctionnera pas?
ChaosPandion
3
Fondamentalement, vous devez utiliser TryAdd au lieu du commun Contains -> Add.
ChaosPandion
3
@Bruno: Non. Si vous ne l'avez pas verrouillé, un autre thread peut avoir supprimé la clé entre les deux appels.
Mark Byers
13
Je ne veux pas paraître brusque ici, mais cela TryGetValuea toujours fait partie de Dictionaryet a toujours été l'approche recommandée. Les méthodes "concurrentes" importantes qui ConcurrentDictionaryintroduisent sont AddOrUpdateet GetOrAdd. Donc, bonne réponse, mais j'aurais pu choisir un meilleur exemple.
Aaronaught
4
Droite - contrairement à la base de données qui a une transaction, la plupart des structures de recherche simultanées en mémoire manquent le concept d' isolement , elles garantissent seulement que la structure de données interne est correcte.
Chang
39

En interne, ConcurrentDictionary utilise un verrou distinct pour chaque compartiment de hachage. Tant que vous n'utilisez que Add / TryGetValue et les méthodes similaires qui fonctionnent sur des entrées uniques, le dictionnaire fonctionnera comme une structure de données presque sans verrouillage avec l'avantage de performances doux respectif. OTOH les méthodes d'énumération (y compris la propriété Count) verrouillent tous les compartiments à la fois et sont donc pires qu'un dictionnaire synchronisé, en termes de performances.

Je dirais, utilisez simplement ConcurrentDictionary.

Stefan Dragnev
la source
15

Je pense que la méthode ConcurrentDictionary.GetOrAdd est exactement ce dont la plupart des scénarios multi-threads ont besoin.

Konstantin
la source
1
J'utiliserais TryGet également, sinon vous perdez l'effort d'initialiser le deuxième paramètre à GetOrAdd chaque fois que la clé existe déjà.
Jorrit Schippers
7
GetOrAdd a la surcharge GetOrAdd (TKey, Func <TKey, TValue>) , de sorte que le deuxième paramètre ne peut être initialisé tardivement que si la clé n'existe pas dans le dictionnaire. En outre TryGetValue est utilisé pour récupérer des données, pas pour les modifier. Les appels ultérieurs à chacune de ces méthodes peuvent provoquer qu'un autre thread peut avoir ajouté ou supprimé la clé entre les deux appels.
sgnsajgon
Cela devrait être la réponse claire à la question, même si le message de Lasse est excellent.
Spivonious
13

Avez-vous vu les extensions réactives pour .Net 3.5sp1. Selon Jon Skeet, ils ont rétroporté un ensemble d'extensions parallèles et de structures de données simultanées pour .Net3.5 sp1.

Il existe un ensemble d'exemples pour .Net 4 Beta 2, qui décrit assez bien comment les utiliser avec les extensions parallèles.

Je viens de passer la semaine dernière à tester le ConcurrentDictionary en utilisant 32 threads pour effectuer des E / S. Cela semble fonctionner comme annoncé, ce qui indiquerait qu'une quantité considérable de tests y a été effectuée.

Éditer : .NET 4 ConcurrentDictionary et modèles.

Microsoft a publié un pdf appelé Patterns of Paralell Programming. Il vaut vraiment la peine d'être téléchargé car il décrit dans de très beaux détails les bons modèles à utiliser pour les extensions .Net 4 Concurrent et les anti-modèles à éviter. C'est ici.

fluage portée
la source
4

Fondamentalement, vous souhaitez utiliser le nouveau ConcurrentDictionary. Dès la sortie de la boîte, vous devez écrire moins de code pour créer des programmes thread-safe.

ChaosPandion
la source
y a-t-il un problème si je continue avec Concurrent Dictionery?
kbvishnu
1

C'est ConcurrentDictionaryune excellente option si elle répond à tous vos besoins en matière de sécurité des threads. Si ce n'est pas le cas, c'est-à-dire que vous faites quelque chose de légèrement complexe, un Dictionary+ normal lockpeut être une meilleure option. Par exemple, disons que vous ajoutez des commandes dans un dictionnaire et que vous souhaitez garder à jour le montant total des commandes. Vous pouvez écrire un code comme celui-ci:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Ce code n'est pas thread-safe. Plusieurs threads mettant à jour le _totalAmountchamp peuvent le laisser dans un état corrompu. Vous pouvez donc essayer de le protéger avec un lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Ce code est "plus sûr", mais toujours pas thread-safe. Il n'y a aucune garantie que le _totalAmountest cohérent avec les entrées du dictionnaire. Un autre thread peut essayer de lire ces valeurs, pour mettre à jour un élément d'interface utilisateur:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

Le totalAmountpeut ne pas correspondre au nombre de commandes dans le dictionnaire. Les statistiques affichées peuvent être erronées. À ce stade, vous vous rendrez compte que vous devez étendre la lockprotection pour inclure la mise à jour du dictionnaire:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Ce code est parfaitement sûr, mais tous les avantages de l'utilisation d'un ConcurrentDictionaryont disparu.

  1. Les performances sont devenues pires qu'avec une utilisation normale Dictionary, car le verrouillage interne à l'intérieur duConcurrentDictionary est désormais inutile et redondant.
  2. Vous devez examiner tout votre code pour les utilisations non protégées des variables partagées.
  3. Vous êtes coincé avec l'utilisation d'une API maladroite ( TryAdd?, AddOrUpdate?).

Mon conseil est donc le suivant: commencez par un Dictionary+ locket conservez la possibilité de passer ultérieurement à une ConcurrentDictionaryoptimisation des performances, si cette option est réellement viable. Dans de nombreux cas, ce ne sera pas le cas.

Theodor Zoulias
la source
0

Nous avons utilisé ConcurrentDictionary pour la collection mise en cache, qui est re-remplie toutes les 1 heure, puis lue par plusieurs threads clients, similaire à la solution pour cet exemple thread-safe?question.

Nous avons constaté que le changement de  ReadOnlyDictionary  améliorait les performances globales.

Michael Freidgeim
la source
ReadOnlyDictionary n'est que le wrapper d'un autre IDictionary. Qu'est-ce que vous utilisez sous le capot?
Lu55
@ Lu55, nous utilisions la bibliothèque MS .Net et choisissons parmi les dictionnaires disponibles / applicables. Je ne suis pas au courant de l'implémentation interne MS de ReadOnlyDictionary
Michael Freidgeim