Supposons qu'une classe possède un champ public int counter
auquel plusieurs threads ont accès. Cette int
est seulement incrémentée ou décrémentée.
Pour incrémenter ce domaine, quelle approche faut-il utiliser et pourquoi?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,counter
par 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 faire cela?
Remplacez le modificateur d'accès de
counter
parpublic volatile
Comme d'autres personnes l'ont mentionné, cela en soi n'est pas du tout sécuritaire. L'intérêt 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 notvolatile
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 causer des problèmes.
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.
lock(this.locker) this.counter++
;
Ceci est sans danger (à condition que vous vous souveniez de lock
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 de 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
dans un autre endroit qui n’est pas vraiment lié, vous pouvez bloquer vos autres threads sans raison.
Interlocked.Increment(ref this.counter);
Ceci est sûr, car la lecture, l’incrémentation et l’écriture s’effectuent de manière efficace, ce qui ne peut être interrompu. 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 littéralement d'une instruction de processeur unique).
Cependant, je ne suis pas tout à fait sûr que les autres processeurs réordonnent ou que vous ayez également besoin de combiner volatile et incrément.
Notes entrelacées:
Comme volatile
n'empêche pas ce type de problèmes de multithreading, à quoi ça sert? 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 serait de 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 uniquement fonctionne si vous avez des écrivains qui ne lisent jamais et des lecteurs qui n'écrivent jamais, et si ce que vous écrivez est une valeur atomique. Dès que vous effectuez une seule lecture-modification-écriture, vous devez accéder aux opérations Interlocked ou utiliser un verrou.
EDIT: Comme indiqué dans les commentaires, je suis heureux de pouvoir utiliser Interlocked
pour les cas d'une variable unique où c'est évidemment d'accord. Quand ça devient plus compliqué, je vais quand même revenir au verrouillage ...
L'utilisation de volatile
ne vous aidera pas lorsque vous aurez besoin d'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 d’obtenir une solution qui est évidemment juste 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 moi-même le filetage, j’essaie de rester simple.
"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:
Interlocked.Increment
exécute les trois parties en une seule opération atomique.
Ce que vous recherchez est soit le verrouillage, soit l’augmentation incrémentée.
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 toujours) en le vérifiant par rapport à un registre de CPU (par exemple, EAX, car c'était 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 (ceci 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 () n’émet pas des barrières en lecture/écriture comme l’impliquent les publications précédentes qui disaient "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 sont toujours garantis de prendre leur retraite dans l'ordre - sinon nous sommes bourrés!
Avec Interlocked Increment, le processeur doit sortir, récupérer la valeur de l'adresse indiquée, puis l'incrémenter et l'écrire - tout en ayant la propriété exclusive de la ligne de cache complète (lock xadd) pour s'assurer qu'aucun autre processeur ne peut le modifier. Sa valeur.
Avec volatile, vous n'aurez toujours 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. Ainsi, au lieu d’avoir incrémenté la valeur deux fois, vous n’obtenez qu’une fois.
J'espère que cela clarifie le problème.
Pour plus d'informations, voir 'Comprendre l'impact des techniques de verrouillage réduit 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 possibilité de changement de contexte lors de l'incrément. Il n'y a donc aucune chance d'impasse ou d'attente.
Je dirais que vous devriez toujours préférer cela à un verrou et à une incrémentation.
Volatile est utile si vous avez besoin que des é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 événements se produisent dans un autre thread que l'optimiseur ignore). C'est un choix orthogonal à la façon dont vous incrémentez.
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'aborder
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 d'extraire puis d'écrire une valeur modifiée peut toujours être en conflit avec un autre thread qui répète l'opération.
J'appuie la réponse de Jon Skeet et souhaite ajouter les liens suivants à tous ceux qui souhaitent en savoir plus sur "volatile" et Interlocked:
Atomicité, volatilité et immuabilité sont différentes, deuxième partie
Atomicité, volatilité et immuabilité sont différentes, troisième partie
Sayonara Volatile - (Instantané Wayback Machine du Weblog de Joe Duffy tel qu'il est apparu en 2012)
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 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. Gardez à l'esprit que le test implique également la lecture sécurisée après augmentation, ce qui est typique du 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
Je voudrais ajouter à mentionné dans les autres réponses la différence entre volatile
, Interlocked
et lock
:
Le mot clé volatile peut être appliqué à des champs de ces types :
sbyte
, byte
, short
, ushort
, int
, uint
, char
, float
et bool
.byte
, sbyte
, short
, ushort, int
ou uint
.IntPtr
et UIntPtr
. Les autres types , y compris double
et long
, ne peuvent pas être marqués "volatile", car les lectures et écritures sur les champs de ces types ne peuvent pas être garanti d'être atomique. Pour protéger l'accès multithread à ces types de champs, utilisez les membres de la classe Interlocked
ou protégez l'accès à l'aide de l'instruction lock
.