Volatile vs. Interlocked vs lock

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

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

  • lock(this.locker) this.counter++; ,
  • Interlocked.Increment(ref this.counter); ,
  • Modifiez le modificateur d’access du counter à public volatile .

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

Le pire (ne fonctionnera pas réellement)

Changer le modificateur d’access de counter à public volatile

Comme d’autres personnes l’ont mentionné, ce n’est pas une solution sûre du tout. Le sharepoint volatile est que plusieurs threads exécutés sur plusieurs processeurs peuvent et vont mettre en cache les données et les instructions de ré-ordonnancement.

Si elle n’est pas volatile et que la CPU A incrémente une valeur, alors la CPU B ne verra peut-être pas cette valeur incrémentée avant un certain temps, ce qui peut causer des problèmes.

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

Deuxième meilleur:

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

Ceci est sûr à faire (à condition que vous vous this.counter de lock partout où vous accédez à this.counter ). Il 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 du multi-processeur comme ci-dessus, ce qui est excellent.

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

Meilleur

Interlocked.Increment(ref this.counter);

Ceci est sûr, car il effectue efficacement la lecture, l’incrémentation et l’écriture en “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 rappeler de verrouiller ailleurs non plus. C’est aussi très rapide (comme MSDN le dit, sur les processeurs modernes, il s’agit souvent d’une seule instruction de processeur).

Je ne suis toutefois pas tout à fait sûr si cela permet de contourner d’autres choses concernant la réorganisation des processeurs, ou si vous devez également combiner l’instabilité avec l’instabilité.

Notes nestedes:

  1. LES MÉTHODES DE VERROUILLAGE SONT CONCURRENTIELLEMENT SÉCURITAIRES SUR TOUT NOMBRE DE NOYAUX OU DE CPU.
  2. Les méthodes interverrouillées appliquent une clôture complète autour des instructions qu’elles exécutent, de sorte que le réordonnancement ne se produit pas.
  3. Les méthodes interverrouillées n’ont pas besoin, ni même ne permettent pas l’access à un champ volatile , car une volatilité est placée une demi-clôture autour des opérations sur un champ donné et l’interlocked utilise la clôture complète.

Note de bas de page: Ce qui est volatil est réellement bon.

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 dans une variable (par exemple, 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 (voire potentiellement dans le mauvais ordre).

Une solution serait de verrouiller, mais vous pourriez également utiliser volatile dans cette situation. Cela garantirait que le thread B verra toujours la chose la plus récente écrite par le thread A. Notez cependant que cette logique ne fonctionne que si vous avez des auteurs 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 lecture-modification-écriture unique, vous devez accéder aux opérations verrouillées ou utiliser un verrou.

EDIT: Comme noté dans les commentaires, ces jours-ci, je suis heureux d’utiliser Interlocked pour les cas d’une seule variable où c’est évidemment bien. Quand ça deviendra plus compliqué, je reviendrai toujours au locking …

L’utilisation de volatile ne vous aidera pas lorsque vous devez incrémenter – car la lecture et l’écriture sont des instructions distinctes. Un autre thread peut changer la valeur après avoir lu, mais avant de réécrire.

Personnellement, je ne fais presque que verrouiller – il est plus facile de bien faire les choses que ce qui est évident, que ce soit la volatilité ou l’Incrément. En ce qui me concerne, le multi-threading sans locking 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 parallèliseront les choses sans verrouiller autant 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 restr simple.

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 fait trois opérations:

  1. lis
  2. incrément
  3. écrire

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

Ce que vous recherchez est le locking ou l’incrémentation croisée.

La volatilité n’est certainement pas ce que vous recherchez – elle dit simplement au compilateur de traiter la variable comme changeant toujours, même si le chemin de code actuel permet au compilateur d’optimiser une lecture de la mémoire sinon.

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é comme volatile, le compilateur est libre d’en faire une boucle infinie (mais cela ne veut pas toujours dire) en le vérifiant dans un registre CPU (par exemple, EAX ce que m_Var a été récupéré depuis le début) au lieu de lancer une autre lecture à l’emplacement mémoire de m_Var (cela peut être mis en cache – nous ne soaps pas et ne nous inquiétons pas et c’est le sharepoint cohérence du cache x86 / x64). Tous les articles publiés plus tôt par d’autres qui ont mentionné la réorganisation des instructions montrent simplement qu’ils ne comprennent pas les architectures x86 / x64. Volatile n’émet pas de barrières de lecture / écriture, comme le suggèrent les articles précédents en disant “cela empêche la réorganisation”. En fait, grâce au protocole MESI, nous sums assurés que le résultat que nous lisons est toujours le même sur tous les processeurs, que les résultats réels aient été retirés dans la mémoire physique ou simplement dans le cache du processeur local. Je ne vais pas aller trop loin dans les détails, mais soyez assuré que si cela ne va pas, Intel / AMD émettra probablement un rappel de processeur! Cela signifie également que nous n’avons pas à nous soucier de l’exécution des commandes en suspens, etc. Les résultats sont toujours garantis à la retraite dans l’ordre – sinon, nous sums bourrés!

Avec Increment verrouillé, le processeur doit sortir, récupérer la valeur de l’adresse indiquée, puis l’incrémenter et le réécrire – tout cela en étant propriétaire exclusif de la ligne de cache entière (verrouiller xadd) pour s’assurer qu’aucun autre processeur ne puisse modifier Sa valeur.

Avec volatile, vous finirez toujours avec une seule instruction (en supposant que 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 sur m_Var après sa lecture 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 élimine le problème.

Pour plus d’informations, voir «Comprendre l’impact des techniques de faible locking dans les applications multithread» – http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Qu’est-ce qui a provoqué cette réponse très tardive? Toutes les réponses étaient manifestement incorrectes (en particulier celle marquée comme réponse) dans leur explication. Je devais juste clarifier cela pour quiconque lirait 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 gâché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 puissant afin qu’il soit cohérent sur toutes les plates-formes – sinon 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 lui-même reconnu – http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Les fonctions verrouillées ne se bloquent pas. Ils sont atomiques, ce qui signifie qu’ils peuvent se compléter sans la possibilité d’un changement de contexte pendant l’incrémentation. Il n’y a donc aucune chance de blocage ou d’attente.

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

Volatile est utile si vous avez besoin d’écrire dans un thread à lire 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 à l’incrémentation.

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

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

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

Interlocked. * Est la manière correcte de le faire … beaucoup moins que les CPU modernes en font une primitive.

volatil 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 effectuant la même opération.

J’appuie la réponse de Jon Skeet et je souhaite append les liens suivants à tous ceux qui veulent en savoir plus sur “volatile” et Interlocked:

La première, l’atomicité, la volatilité et l’immuabilité sont différentes – (Fabulous Adventures In Coding d’Eric Lippert)

Atomicité, volatilité et immuabilité sont différentes, deuxième partie

Atomicité, volatilité et immuabilité sont différentes, troisième partie

Sayonara Volatile – (Un instantané du Weblog de Joe Duffy paru en 2012)

J’ai fait des tests pour voir comment fonctionne la théorie: 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. Interlocked n’est pas nécessaire plus rapidement dans un environnement multi-processeurs. Voici le résultat du test pour Increment sur un serveur 16 CPU vieux 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 

Lisez le filetage dans la référence C # . Il couvre les tenants et les aboutissants de votre question. Chacun des trois a des objectives différents et des effets secondaires.