Question Volatile vs. Interverrouillé vs.


Disons qu'une classe a un public int counter champ auquel plusieurs threads accèdent. Ce int est seulement incrémenté ou décrémenté.

Pour incrémenter ce champ, quelle approche devrait être utilisée, et pourquoi?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Changer le modificateur d'accès de counter à public volatile.

Maintenant que j'ai découvert volatile, J'enlève beaucoup lock déclarations et l'utilisation de Interlocked. Mais y a-t-il une raison de ne pas le faire?


586
2017-09-30 19:25


origine


Réponses:


Pire (ne travaillera pas réellement)

Changer le modificateur d'accès de counter à public volatile

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

Si c'est ne pas  volatileet la CPU A incrémente une valeur, alors la CPU B peut ne pas voir cette valeur incrémentée jusqu'à un certain temps, ce qui peut causer des problèmes.

Si c'est volatile, cela garantit simplement que les deux CPU voient les mêmes données en même temps. Cela ne les empêche pas du tout d'entrelacer leurs lectures et opérations d'écriture, ce qui est le problème que vous essayez d'éviter.

Deuxième meilleur:

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

C'est sûr à faire (à condition de vous souvenir de lock partout ailleurs où vous avez accès this.counter). Il empêche les autres threads d'exécuter tout autre code protégé par locker. L'utilisation de verrous empêche également les problèmes de réordonnancement multi-CPU comme ci-dessus, ce qui est génial.

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

Meilleur

Interlocked.Increment(ref this.counter);

C'est sûr, car il fait effectivement la lecture, l'incrémentation et l'écriture dans 'un coup' qui ne peut pas être interrompu. Pour cette raison, cela n'affectera aucun autre code, et vous n'avez pas besoin de vous souvenir de verrouiller ailleurs. Il est également très rapide (comme le dit MSDN, sur les processeurs modernes, il s'agit littéralement d'une seule instruction CPU).

 Cependant, je ne suis pas tout à fait sûr si cela se déplace autour d'autres processeurs réordonnant des choses, ou si vous devez également combiner volatile avec l'incrément.

InterlockedNotes:

  1. LES MÉTHODES DE VERROUILLAGE SONT SIMULTANEMENT SÛRES SUR TOUT NOMBRE DE CORES OU DE CPU.
  2. Les méthodes imbriquées appliquent une clôture complète autour des instructions qu'elles exécutent, de sorte que la réorganisation ne se produit pas.
  3. Méthodes interverrouillées ne nécessite pas ou même ne prend pas en charge l'accès à un champ volatile, comme volatil est placé une demi-clôture autour des opérations sur un champ donné et interverrouillé utilise la clôture complète.

Note de bas de page: Quel volatile est réellement bon pour.

Comme volatile n'empêche pas ce genre de problèmes de multithreading, à quoi ça sert? Un bon exemple est de dire que vous avez deux threads, un qui écrit toujours à une variable (par exemple queueLength), et celui 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 différées (voire potentiellement dans le mauvais ordre).

Une solution serait de verrouiller, mais vous pouvez également utiliser volatile dans cette situation. Cela garantirait que le thread B verra toujours la chose la plus à jour que le thread A a écrite. 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 faites une seule lecture-modification-écriture, vous devez passer à des opérations inter-verrouillées ou utiliser un verrou.


754
2017-09-30 20:13



MODIFIER: Comme indiqué dans les commentaires, ces jours je suis heureux d'utiliser Interlocked pour les cas d'un variable unique où c'est évidemment d'accord. Quand ça devient plus compliqué, je vais quand même revenir au verrouillage ...

En utilisant volatile n'aidera pas lorsque vous avez besoin d'incrémenter - parce que la lecture et l'écriture sont des instructions séparées. Un autre thread pourrait changer la valeur après que vous ayez lu, mais avant de l'écrire.

Personnellement, je verrouille presque toujours - il est plus facile de faire les choses correctement évidemment juste que soit la volatilité ou Interlocked.Increment. En ce qui me concerne, le multi-threading sans verrou est destiné aux vrais experts en threading, dont je ne suis pas un. Si Joe Duffy et son équipe construisent de jolies bibliothèques qui vont paralléliser les choses sans autant de verrouillage que ce que je construirais, c'est fabuleux, et je vais l'utiliser en un clin d'œil - mais quand je fais le thread moi-même, j'essaie de rester simple.


125
2017-09-30 19:29



"volatile"ne remplace pas Interlocked.Increment! Il s'assure juste 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. lis
  2. incrément
  3. écrire

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


41
2017-09-30 19:32



L'incrément de verrouillage ou d'incrémentation est ce que vous recherchez.

Volatile n'est certainement pas ce que vous recherchez - il indique simplement au compilateur de traiter la variable comme étant toujours changeante même si le chemin de code actuel permet au compilateur d'optimiser une lecture depuis la mémoire.

par exemple.

while (m_Var)
{ }

si m_Var est mis à false dans un autre thread mais qu'il n'est pas déclaré volatile, le compilateur est libre de faire une boucle infinie (mais ne le veut pas toujours) en le faisant vérifier par rapport à un registre CPU (par exemple EAX car ce que m_Var a été récupéré depuis le début) au lieu d'émettre une autre lecture à l'emplacement mémoire de m_Var (ceci peut être mis en cache - nous ne savons pas et ne nous intéressons pas et c'est le point de cohérence du cache de x86 / x64). Tous les articles publiés précédemment par d'autres personnes qui ont mentionné la réorganisation des instructions montrent simplement qu'ils ne comprennent pas les architectures x86 / x64. Volatile ne pas émettre des barrières de lecture / écriture, comme le suggèrent les messages antérieurs en disant «cela empêche de réordonner». En fait, grâce au protocole MESI, nous sommes assurés que le résultat que nous lisons est toujours le même sur les CPU, que les résultats réels aient été retirés de la mémoire physique ou simplement stockés dans le cache du processeur local. Je n'irai pas trop loin dans les détails de ceci mais soyez assurés que si cela ne va pas, Intel / AMD émettrait probablement un rappel de processeur! Cela signifie également que nous n'avons pas à nous préoccuper de l'exécution hors d'état de la procédure. Les résultats sont toujours garantis de se retirer dans l'ordre - sinon nous sommes bourrés!

Avec Increment Interlocked, le processeur doit sortir, extraire la valeur de l'adresse donnée, puis l'incrémenter et l'écrire - tout cela en ayant la propriété exclusive de toute la ligne de cache (lock xadd) pour s'assurer qu'aucun autre processeur ne peut modifier Sa valeur.

Avec volatile, vous finirez avec seulement 1 instruction (en supposant que le JIT est 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 interlockée. Comme vous pouvez l'imaginer, cela signifie que d'autres processeurs peuvent écrire une valeur mise à jour dans m_Var après qu'elle a été lue par cpuA. Donc, au lieu d'avoir maintenant incrémenté la valeur deux fois, vous vous retrouvez avec une seule fois.

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

Pour plus d'informations, voir "Comprendre l'impact des techniques à faible verrouillage dans les applications multithread" - http://msdn.microsoft.com/fr-fr/magazine/cc163715.aspx

p.s. Qu'est-ce qui a provoqué cette réponse très tardive? Toutes les réponses étaient si faussement incorrectes (en particulier celle qui était marquée comme réponse) dans leur explication que je devais juste la clarifier pour quiconque la lisant. hausse

p.p.s. Je suppose que la cible est x86 / x64 et non IA64 (elle a un modèle de mémoire différent). Notez que les spécifications ECMA de Microsoft sont erronées car elles spécifient le modèle de mémoire le plus faible au lieu du plus fort (il est toujours préférable de spécifier le modèle de mémoire le plus fort pour qu'il soit cohérent sur toutes les plates-formes). 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.


38
2018-06-23 15:08



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

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

Volatile est utile si vous avez besoin d'écrire dans un thread pour être lu dans un autre, et si vous voulez que l'optimiseur ne réorganise pas les opérations sur une variable (car les choses se passent dans un autre thread que l'optimiseur ignore). C'est un choix orthogonal à la façon dont vous incrémentez.

Ceci est un très bon article si vous voulez en savoir plus sur le code sans verrouillage, et la bonne façon d'aborder l'écriture

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


15
2017-09-30 19:27



lock (...) fonctionne, mais peut bloquer un thread et provoquer un blocage si un autre code utilise les mêmes verrous d'une manière incompatible.

Interlocked. * Est la bonne façon de le faire ... beaucoup moins de frais généraux que les processeurs modernes supportent cela comme une primitive.

volatile en soi n'est pas correct. Un thread essayant d'extraire puis de réécrire une valeur modifiée peut toujours entrer en conflit avec un autre thread faisant la même chose.


11
2017-09-30 19:32



Je seconde la réponse de Jon Skeet et je veux ajouter les liens suivants pour tous ceux qui veulent en savoir plus sur "volatile" et Interlocked:

L'atomicité, la volatilité et l'immuabilité sont différentes, première partie - (Fabulous Adventures In Coding d'Eric Lippert)

L'atomicité, la volatilité et l'immuabilité sont différentes, deuxième partie 

L'atomicité, la volatilité et l'immuabilité sont différentes, troisième partie

Sayonara Volatile - (Instantané de Wayback Machine du Weblog de Joe Duffy tel qu'il est apparu en 2012) 


8
2018-04-29 22:04



J'ai fait un test pour voir comment la théorie fonctionne 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. Interverrouillé n'est pas nécessaire plus rapidement dans un environnement multi-cpu. Voici le résultat du test pour Increment sur un serveur 16 CPU de 2 ans. Gardez à l'esprit 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

6
2018-05-24 23:12