Supposons qu'une classe possède un public int counter
champ accessible par plusieurs threads. Ceci int
est seulement incrémenté ou décrémenté.
Pour incrémenter ce champ, quelle approche utiliser et pourquoi?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Remplacez le modificateur d'accès
counter
parpublic volatile
.
Maintenant que j'ai découvert volatile
, j'ai supprimé de nombreuses lock
déclarations et l'utilisation de Interlocked
. Mais y a-t-il une raison de ne pas le faire?
Réponses:
Pire (ne fonctionnera pas réellement)
Comme d'autres l'ont mentionné, cela n'est pas du tout sûr en soi. Le fait
volatile
est que plusieurs threads s'exécutant sur plusieurs processeurs peuvent et vont mettre en cache les données et réorganiser les instructions.Si ce n'est pas le cas
volatile
et que le CPU A incrémente une valeur, le CPU B peut ne pas réellement voir cette valeur incrémentée jusqu'à un certain temps plus tard, ce qui peut provoquer des problèmes.Si c'est le cas
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 opérations de lecture et d'écriture, ce que vous essayez d'éviter.Deuxième meilleur:
Ceci est sûr à faire (à condition de vous souvenir de
lock
partout où vous accédezthis.counter
). Il empêche tout autre thread d'exécuter tout autre code protégé parlocker
. L'utilisation de verrous empêche également les problèmes de réorganisation multi-CPU comme ci-dessus, ce qui est génial.Le problème est que 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 threads sans raison.Meilleur
Ceci est sûr, car il effectue efficacement la lecture, l'incrémentation et l'écriture en un seul coup qui ne peut pas être interrompu. Pour cette raison, cela n'affectera aucun autre code, et vous n'avez pas besoin de vous rappeler de verrouiller ailleurs non plus. C'est aussi très rapide (comme le dit MSDN, sur les processeurs modernes, il s'agit souvent littéralement d'une seule instruction de processeur).
Je ne suis pas tout à fait sûr cependant si cela contourne les autres processeurs réorganisant les choses, ou si vous devez également combiner volatile avec l'incrément.InterlockedNotes:
Note de bas de page: Ce qui est réellement bon volatile.
Comme cela
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 dans une variable (disonsqueueLength
) et un qui lit toujours à partir de cette même variable.S'il
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 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 écrite par le thread A. Notez cependant que cette logique ne fonctionne que 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 verrouillées ou utiliser un verrou.
la source
EDIT: Comme indiqué dans les commentaires, ces jours-ci, je suis heureux d'utiliser
Interlocked
pour les cas d'une seule variable où c'est évidemment correct. Quand ça devient plus compliqué, je vais quand même revenir au verrouillage ...L'utilisation
volatile
n'aidera pas lorsque vous devez incrémenter - car la lecture et l'écriture sont des instructions distinctes. Un autre thread pourrait changer la valeur après avoir lu mais avant de réécrire.Personnellement, je verrouille presque toujours - il est plus facile de bien faire d'une manière qui est évidemment juste que la volatilité ou l'interverrouillage. En ce qui me concerne, le multi-threading sans verrouillage est pour les vrais experts du threading, dont je ne suis pas un. Si Joe Duffy et son équipe créent de belles bibliothèques qui parallèleront les choses sans autant de verrouillage que quelque chose que je construirais, c'est fabuleux, et je l'utiliserai en un clin d'œil - mais quand je fais le filetage moi-même, j'essaie de rester simple.
la source
"
volatile
" ne remplace pasInterlocked.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 fait trois opérations:
Interlocked.Increment
effectue les trois parties comme une seule opération atomique.la source
volatile
ne s'assure pas que la variable n'est pas mise en cache. Il met simplement des restrictions sur la façon dont il peut être mis en cache. Par exemple, il peut toujours être mis en cache dans des éléments du cache L2 du processeur, car ils sont rendus cohérents dans le matériel. Il peut encore être préfet. Les écritures peuvent toujours être publiées dans le cache, etc. (Je pense que c'était ce à quoi Zach voulait en venir.)Soit verrouillé, soit incrémenté, c'est ce que vous recherchez.
Volatile n'est certainement pas ce que vous recherchez - il dit simplement au compilateur de traiter la variable comme toujours changeante même si le chemin de code actuel permet au compilateur d'optimiser une lecture de la mémoire autrement.
par exemple
si m_Var est défini sur false dans un autre thread mais qu'il n'est pas déclaré comme volatile, le compilateur est libre d'en faire une boucle infinie (mais cela ne signifie pas toujours) en le comparant à un registre CPU (par exemple EAX parce que c'était dans lequel m_Var a été récupéré dès le début) au lieu d'émettre une autre lecture à l'emplacement de mémoire de m_Var (cela peut être mis en cache - nous ne savons pas et ne nous soucions pas et c'est le point de cohérence du cache de x86 / x64). Tous les articles précédents par d'autres qui ont mentionné la réorganisation des instructions montrent simplement qu'ils ne comprennent pas les architectures x86 / x64. Volatile ne fait pasémettre des barrières en lecture / écriture comme le suggèrent les articles précédents en disant «cela empêche la réorganisation». En fait, grâce encore au protocole MESI, nous sommes assurés que le résultat que nous lisons est toujours le même sur tous les CPU, que les résultats réels aient été retirés de la mémoire physique ou résident simplement dans le cache du CPU local. Je n'irai pas trop loin dans les détails, mais soyez assuré que si cela se passe mal, Intel / AMD émettra 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 sont toujours garantis de se retirer dans l'ordre - sinon nous sommes bourrés!
Avec l'incrémentation verrouillée, le processeur doit sortir, récupérer la valeur à partir de l'adresse donnée, puis l'incrémenter et la réécrire - tout cela tout en ayant la propriété exclusive de toute la ligne de cache (lock xadd) pour vous assurer qu'aucun autre processeur ne peut modifier Sa valeur.
Avec volatile, vous vous retrouverez toujours avec une seule 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 verrouillée. Comme vous pouvez l'imaginer, cela signifie que d'autres processeurs pourraient réécrire une valeur mise à jour dans m_Var après avoir été lue par cpuA. Ainsi, au lieu d'avoir maintenant incrémenté la valeur deux fois, vous vous retrouvez avec une seule fois.
J'espère que cela clarifie le problème.
Pour plus d'informations, voir «Comprendre l'impact des techniques de faible verrouillage dans les applications multithread» - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
ps 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 simplement clarifier pour quiconque lisant ceci. hausse les épaules
pps 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 vissées en ce sens qu'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 par rapport au modèle de mémoire le plus fort afin qu'il soit cohérent sur toutes les plates-formes - sinon le code s'exécuterait 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 .
la source
Les fonctions verrouillées ne se verrouillent pas. Ils sont atomiques, ce qui signifie qu'ils peuvent se terminer sans la possibilité d'un changement de contexte pendant l'incrémentation. Il n'y a donc aucune chance de blocage ou d'attendre.
Je dirais que vous devriez toujours le préférer à un verrou et à un incrément.
Volatile est utile si vous avez besoin que les écritures dans un thread soient lues dans un autre 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 ne connaît pas). C'est un choix orthogonal à la façon dont vous incrémentez.
Ceci est un très bon article si vous souhaitez en savoir plus sur le code sans verrou et la bonne façon d'aborder l'écriture
http://www.ddj.com/hpc-high-performance-computing/210604448
la source
lock (...) fonctionne, mais peut bloquer un thread et peut provoquer 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 frais généraux que les processeurs modernes prennent en charge cela comme une primitive.
volatile en soi n'est pas correct. Un thread tentant de récupérer puis de réécrire une valeur modifiée peut toujours entrer en conflit avec un autre thread faisant de même.
la source
J'ai fait quelques tests pour voir comment la théorie fonctionne réellement: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Mon test était plus axé sur CompareExchnage mais le résultat pour Increment est similaire. L'interverrouillage 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 du monde réel.
la source
J'appuie 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:
Atomicité, volatilité et 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é Wayback Machine du blog de Joe Duffy tel qu'il est apparu en 2012)
la source
Je voudrais ajouter mentionné dans les autres réponses à la différence entre
volatile
,Interlocked
etlock
:Le mot-clé volatile peut être appliqué aux champs de ces types :
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
etbool
.byte
,sbyte
,short
, ushort,int
ouuint
.IntPtr
etUIntPtr
.D'autres types , y compris
double
etlong
, ne peuvent pas être marqués «volatils» car les lectures et les écritures dans les champs de ces types ne peuvent pas être garanties atomiques. Pour protéger l'accès multithread à ces types de champs, utilisez lesInterlocked
membres de la classe ou protégez l'accès à l'aide de l'lock
instruction.la source