Question

Supposons qu'une classe possède un champ compteur public public auquel plusieurs threads ont accès. Ce int est uniquement incrémenté ou décrémenté.

Pour incrémenter ce champ, quelle approche faut-il utiliser et pourquoi?

  • verrouiller (this.locker) this.counter ++; ,
  • Interlocked.Increment (ref this.counter); ,
  • Modifiez le modificateur d'accès du compteur en public volatile .

Maintenant que j'ai découvert volatile , j'ai supprimé de nombreuses instructions lock et l'utilisation de Interlocked . Mais y a-t-il une raison pour ne pas le faire?

Était-ce utile?

La solution

Le pire (ne fonctionnera pas réellement)

  

Modifiez le modificateur d'accès du compteur en public volatile

.

Comme d'autres personnes l'ont mentionné, cela en soi n'est pas du tout sûr. Le point essentiel de volatile est que plusieurs threads s'exécutant sur plusieurs processeurs peuvent et vont mettre en cache des données et réorganiser des instructions.

Si elle n'est pas volatile et que la CPU A incrémente une valeur, la CPU B peut ne pas voir cette valeur incrémentée avant un certain temps, ce qui peut poser problème.

S'il s'agit de volatile , cela garantit simplement que les deux processeurs voient les mêmes données en même temps. Cela ne les empêche pas du tout d'entrelacer leurs lectures et écritures, problème que vous essayez d'éviter.

Deuxième meilleur:

  

verrouiller (this.locker) this.counter ++ ;

Ceci est sans danger (à condition que vous vous souveniez de verrouiller partout où vous accédez à this.counter ). Cela empêche tout autre thread d'exécuter tout autre code protégé par locker . L’utilisation des verrous empêche également les problèmes de réorganisation multi-CPU comme ci-dessus, ce qui est formidable.

Le problème est que le verrouillage est lent et que si vous réutilisez le locker à un autre endroit qui n'est pas vraiment lié, vous pouvez bloquer vos autres threads sans raison.

Meilleur

  

Interlocked.Increment (ref this.counter);

Ceci est sûr, car il permet effectivement la lecture, l’incrémentation et l’écriture en une frappe qui ne peut être interrompue. De ce fait, cela n’affectera aucun autre code et vous ne devez pas oublier de verrouiller ailleurs. C’est également très rapide (comme le dit MSDN, sur les processeurs modernes, il s’agit souvent d’une simple instruction de processeur).

Je ne suis toutefois pas tout à fait sûr s'il y a d'autres processeurs en train de réorganiser des choses, ou si vous devez également combiner volatile avec l'incrément.

Notes interverrouillées:

  1. LES MÉTHODES VERROUILLÉES SONT SIMPLEMENT SÉCURITAIRES SUR TOUT NOMBRE DE CENTRES OU DE CPU.
  2. Les méthodes interconnectées appliquent une clôture complète autour des instructions qu'elles exécutent. Par conséquent, le réordonnancement n'a pas lieu.
  3. Les méthodes interconnectées ne nécessitent pas, voire ne prennent pas en charge l'accès à un champ volatile , car volatile est placé autour des opérations sur un champ donné et verrouillé utilise la clôture complète.

Note de bas de page: Ce qui est volatile est vraiment bon pour.

Etant donné que volatile n'empêche pas ce type de problèmes liés à la lecture multiple, à quoi cela sert-il? Un bon exemple consiste à dire que vous avez deux threads, un qui écrit toujours dans une variable (disons queueLength ) et un qui lit toujours à partir de cette même variable.

Si queueLength n'est pas volatile, le thread A peut écrire cinq fois, mais le thread B peut voir ces écritures comme étant retardées (ou même potentiellement dans le mauvais ordre).

Une solution consisterait à verrouiller, mais vous pouvez également utiliser volatile dans cette situation. Cela garantirait que le fil B verra toujours la chose la plus récente écrite par le fil A. Notez cependant que cette logique seulement fonctionne si vous avez des écrivains qui ne lisent jamais et des lecteurs qui n'écrivent jamais, et si la chose que vous écrivez est une valeur atomique. Dès que vous effectuez une seule lecture-modification-écriture, vous devez accéder aux opérations liées ou utiliser un verrou.

Autres conseils

EDIT: Comme indiqué dans les commentaires, je suis heureux de pouvoir utiliser Interlocked pour les cas d'une variable unique où < em> évidemment d'accord. Quand cela devient plus compliqué, je vais quand même revenir au verrouillage ...

L'utilisation de volatile ne vous aidera pas si vous devez incrémenter car la lecture et l'écriture sont des instructions distinctes. Un autre fil pourrait changer la valeur après avoir lu mais avant de réécrire.

Personnellement, je verrouille presque toujours - il est plus facile d’obtenir une solution évidemment que la volatilité ou Interlocked.Increment. En ce qui me concerne, le multi-threading sans verrouillage est destiné aux vrais experts du threading, dont je ne suis pas un. Si Joe Duffy et son équipe construisent de belles bibliothèques qui parallèleront les choses sans verrouiller autant que ce que je construirais, c’est fabuleux et je l’utiliserai tout de suite - mais quand je fais le filetage moi-même, j’essaie de restez simple.

& <; code> volatile " ne remplace pas Interlocked.Increment ! Il s'assure simplement que la variable n'est pas mise en cache, mais utilisée directement.

L'incrémentation d'une variable nécessite en réalité trois opérations:

  1. lu
  2. incrémentation
  3. écrire

Interlocked.Increment exécute les trois parties en une seule opération atomique.

Vous recherchez un incrément verrouillé ou un incrément verrouillé.

Volatile n’est certainement pas ce que vous recherchez, il indique simplement au compilateur de traiter la variable comme étant en constante évolution, même si le chemin du code actuel permet au compilateur d’optimiser une lecture à partir de la mémoire.

par exemple

while (m_Var)
{ }

Si m_Var est défini sur false dans un autre thread mais qu'il n'est pas déclaré volatil, le compilateur est libre d'en faire une boucle infinie (mais cela ne veut pas toujours dire qu'il le fera) en le vérifiant par rapport à un registre CPU (par exemple, EAX). car c’est ce que m_Var a été récupéré depuis le tout début) au lieu d’envoyer une autre lecture à l’emplacement mémoire de m_Var (cela peut être mis en cache - nous ne le savons pas et ne nous en soucions pas, c’est le point de cohérence du cache de x86 / x64). Tous les posts précédents de ceux qui ont mentionné la réorganisation des instructions montrent simplement qu'ils ne comprennent pas les architectures x86 / x64. Volatile ne ne crée pas d'obstacles en lecture / écriture comme le laissaient supposer les précédents posts disant "cela empêche la réorganisation". En fait, grâce au protocole MESI, nous sommes assurés que le résultat que nous lisons est toujours le même pour tous les processeurs, que les résultats réels aient été retraités dans la mémoire physique ou qu'ils résident simplement dans le cache du processeur local. Je n’entrerai pas trop dans les détails, mais soyez assurés que si cela ne tournait pas bien, Intel / AMD émettrait probablement un rappel de processeur! Cela signifie également que nous n'avons pas à nous soucier de l'exécution dans le désordre, etc. Les résultats ont toujours la garantie de prendre leur retraite, sinon nous sommes bourrés!

Avec Interlocked Increment, le processeur doit sortir, extraire la valeur de l'adresse indiquée, puis l'incrémenter et la réécrire - tout en ayant la propriété exclusive de la ligne de cache entière (lock xadd) pour s'assurer qu'aucun autre les processeurs peuvent modifier sa valeur.

En mode volatile, vous n’obtenez qu’une instruction (en supposant que le JIT soit efficace comme il se doit) - inc dword ptr [m_Var]. Cependant, le processeur (cpuA) ne demande pas la propriété exclusive de la ligne de cache tout en faisant tout ce qu'il a fait avec la version verrouillée. Comme vous pouvez l'imaginer, cela signifie que d'autres processeurs pourraient écrire une valeur mise à jour dans m_Var après sa lecture par cpuA. Donc, au lieu d’avoir incrémenté la valeur deux fois, vous n’obtenez qu’une fois.

J'espère que cela résout le problème.

Pour plus d'informations, voir 'Comprendre l'impact des techniques de verrouillage faibles dans les applications multithread' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

p.s. Qu'est-ce qui a motivé cette réponse très tardive? Toutes les réponses étaient si manifestement incorrectes (en particulier celle marquée comme réponse) dans leur explication que je devais éclaircir tout cela pour tous ceux qui lisaient ceci. hausse les épaules

p.p.s. Je suppose que la cible est x86 / x64 et non pas IA64 (son modèle de mémoire est différent). Notez que les spécifications ECMA de Microsoft sont faussées en ce sens qu’elles spécifient le modèle de mémoire le plus faible au lieu du modèle le plus puissant (il est toujours préférable de spécifier le modèle de mémoire le plus puissant de manière à ce qu’il soit cohérent sur toutes les plates-formes - sinon le code serait exécuté 24-7 sur x86 / x64 peut ne pas fonctionner du tout sur IA64 bien qu'Intel ait implémenté un modèle de mémoire similaire pour IA64) - Microsoft l'a admis lui-même - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Les fonctions verrouillées ne se verrouillent pas. Ils sont atomiques, ce qui signifie qu'ils peuvent être terminés sans la possibilité d'un changement de contexte pendant l'incrément. Il n'y a donc aucune chance d'impasse ou d'attente.

Je dirais que vous devriez toujours le préférer à un verrou et à une incrémentation.

Volatile est utile si vous avez besoin d'écritures dans un thread, et si vous souhaitez que l'optimiseur ne réorganise pas les opérations sur une variable (car des choses se produisent dans un autre thread que l'optimiseur ignore). C’est un choix orthogonal qui vous permet d’augmenter.

C’est un très bon article si vous voulez en savoir plus sur le code sans verrouillage et la bonne façon de l’écrire

http://www.ddj.com/hpc-high-performance- informatique / 210604448

lock (...) fonctionne, mais peut bloquer un thread et peut entraîner un blocage si un autre code utilise les mêmes verrous de manière incompatible.

Interlocked. * est la bonne façon de le faire ... beaucoup moins de temps système puisque les processeurs modernes supportent cela comme une primitive.

volatile seul n'est pas correct. Un thread qui tente de récupérer puis de réécrire une valeur modifiée peut toujours entrer en conflit avec un autre thread qui fait de même.

J'ai fait quelques essais pour voir comment la théorie fonctionnait réellement: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Mon test était plus concentré sur CompareExchnage mais le résultat pour Increment est similaire. Interlocked n'est pas nécessaire plus rapidement dans un environnement multi-cpu. Voici le résultat du test pour Increment sur un serveur de 16 CPU âgé de 2 ans. N’oubliez pas que le test implique également la lecture sûre après augmentation, ce qui est typique dans le monde réel.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial

J'aimerais ajouter à mentionné dans l'autre réponse la différence entre volatile , Interlocked et lock :

Le mot clé volatile peut être appliqué aux champs de ces types :

  • Types de référence.
  • Types de pointeur (dans un contexte non sécurisé). Notez que bien que le pointeur lui-même puisse être volatile, l'objet sur lequel il pointe ne le peut pas. En d'autre mots, vous ne pouvez pas déclarer un " pointeur " être "volatile".
  • Types simples tels que sbyte , octet , court , ushort , int , uint , char , float et bool .
  • Un type enum avec l'un des types de base suivants: octet , sbyte , short , code court, int ou uint .
  • Paramètres de type génériques connus pour être des types de référence.
  • IntPtr et UIntPtr .

Les autres types , y compris double et long , ne peuvent pas être marqués "volatile". parce que les lectures et écritures sur les champs de ces types ne peuvent pas être garanties être atomique. Pour protéger l’accès multi-thread à ces types de champs, utilisez les membres de la classe Interlocked ou protégez l'accès à l'aide du instruction lock .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top