Volatile contre verrouillé contre verrou

671

Supposons qu'une classe possède un public int counterchamp accessible par plusieurs threads. Ceci intest 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 counterpar public volatile.

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

coeur
la source
Lisez la référence Threading en C # . Il couvre les tenants et aboutissants de votre question. Chacun des trois a des objectifs et des effets secondaires différents.
spoulson
1
simple-talk.com/blogs/2012/01/24/… vous pouvez voir l'utilisation de volitable dans les tableaux, je ne le comprends pas complètement, mais c'est une autre référence à ce que cela fait.
eran otzap
50
C'est comme dire "J'ai découvert que le système de gicleurs n'est jamais activé, donc je vais le retirer et le remplacer par des détecteurs de fumée". La raison de ne pas le faire est parce que c'est incroyablement dangereux et ne vous donne presque aucun avantage . Si vous avez le temps de passer à changer le code, trouvez un moyen de le rendre moins multithread ! Ne trouvez pas un moyen de rendre le code multithread plus dangereux et plus facile à casser!
Eric Lippert
1
Ma maison a des gicleurs et des détecteurs de fumée. Lorsque vous incrémentez un compteur sur un thread et le lisez sur un autre, il semble que vous ayez besoin à la fois d'un verrou (ou d'un verrouillage) et du mot-clé volatile. Vérité?
yoyo
2
@yoyo Non, vous n'avez pas besoin des deux.
David Schwartz

Réponses:

859

Pire (ne fonctionnera pas réellement)

Remplacez le modificateur d'accès par counterparpublic volatile

Comme d'autres l'ont mentionné, cela n'est pas du tout sûr en soi. Le fait volatileest 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:

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

Ceci est sûr à faire (à condition de vous souvenir de lockpartout 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 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 lockerdans un autre endroit qui n'est pas vraiment lié, vous pouvez finir par bloquer vos autres threads sans 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 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:

  1. LES MÉTHODES VERROUILLÉES SONT CONCURRENTEMENT SÉCURITAIRES SUR TOUT NOMBRE DE CŒURS OU DE CPU.
  2. Les méthodes imbriquées appliquent une clôture complète autour des instructions qu'elles exécutent, donc la réorganisation ne se produit pas.
  3. Les méthodes verrouillées n'ont pas besoin ou même ne prennent pas en charge l'accès à un champ volatil , car volatile est placé une demi-clôture autour des opérations sur un champ donné et verrouillé utilise la clôture complète.

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

Comme cela volatilen'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 (disons queueLength) et un qui lit toujours à partir de cette même variable.

S'il queueLengthn'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.

Orion Edwards
la source
29
"Je ne suis pas tout à fait sûr ... si vous devez également combiner volatile et incrément." Ils ne peuvent pas être combinés AFAIK, car nous ne pouvons pas passer d'un volatile par réf. Excellente réponse au fait.
Hosam Aly
41
Merci beaucoup! Votre note de bas de page sur "Ce qui est réellement bon pour les volatiles" est ce que je cherchais et a confirmé comment je veux utiliser les volatiles.
Jacques Bosch
7
En d'autres termes, si un var est déclaré volatile, le compilateur supposera que la valeur du var ne restera pas la même (c'est-à-dire volatile) chaque fois que votre code le rencontrera. Ainsi, dans une boucle telle que: tandis que (m_Var) {} et m_Var est défini sur false dans un autre thread, le compilateur ne vérifiera pas simplement ce qui est déjà dans un registre qui a été précédemment chargé avec la valeur de m_Var mais lit la valeur dans m_Var encore. Cependant, cela ne signifie pas que la non-déclaration de volatile entraînera la boucle à l'infini - la spécification de volatile ne garantit que ce ne sera pas le cas si m_Var est défini sur false dans un autre thread.
Zach Saw
35
@Zach Saw: Dans le modèle de mémoire pour C ++, volatile est la façon dont vous l'avez décrit (essentiellement utile pour la mémoire mappée par périphérique et pas beaucoup d'autre). Selon le modèle de mémoire pour le CLR (cette question est étiquetée C #), la volatilité insérera des barrières de mémoire autour des lectures et des écritures sur cet emplacement de stockage. Les barrières de mémoire (et les variantes verrouillées spéciales de certaines instructions de montage) vous demandent au processeur de ne pas réorganiser les choses, et elles sont assez importantes ...
Orion Edwards
19
@ZachSaw: Un champ volatile en C # empêche le compilateur C # et le compilateur jit de faire certaines optimisations qui mettraient en cache la valeur. Il offre également certaines garanties quant à l'ordre dans lequel les lectures et écritures peuvent être observées sur plusieurs threads. En tant que détail d'implémentation, il peut le faire en introduisant des barrières mémoire sur les lectures et les écritures. La sémantique précise garantie est décrite dans la spécification; notez que la spécification ne garantit pas qu'un ordre cohérent de toutes les écritures et lectures volatiles sera observé par tous les threads.
Eric Lippert
147

EDIT: Comme indiqué dans les commentaires, ces jours-ci, je suis heureux d'utiliser Interlockedpour 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 volatilen'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.

Jon Skeet
la source
16
+1 pour m'avoir assuré d'oublier le codage sans verrou à partir de maintenant.
Xaqron
5
les codes sans verrouillage ne sont certainement pas vraiment sans verrouillage car ils se verrouillent à un moment donné - que ce soit au niveau du bus (FSB) ou au niveau de l'interCPU, il y a toujours une pénalité à payer. Cependant, le verrouillage à ces niveaux inférieurs est généralement plus rapide tant que vous ne saturez pas la bande passante de l'endroit où le verrouillage se produit.
Zach Saw
2
Il n'y a rien de mal avec Interlocked, c'est exactement ce que vous recherchez et plus rapide qu'un verrou complet ()
Jaap
5
@Jaap: Oui, ces jours- ci, j'utiliserais le verrouillage pour un véritable compteur unique. Je ne voudrais tout simplement pas commencer à déconner en essayant de trouver des interactions entre plusieurs mises à jour sans verrouillage des variables.
Jon Skeet
6
@ZachSaw: votre deuxième commentaire dit que les opérations interverrouillées "se verrouillent" à un moment donné; le terme «verrou» implique généralement qu'une tâche peut maintenir le contrôle exclusif d'une ressource pendant une durée illimitée; le principal avantage de la programmation sans verrouillage est qu’elle évite le danger que les ressources deviennent inutilisables à la suite de l’imposition de la tâche propriétaire. La synchronisation de bus utilisée par la classe interverrouillée n'est pas seulement "généralement plus rapide" - sur la plupart des systèmes, elle a un temps limite le plus défavorable, contrairement aux verrous.
supercat
44

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

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

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

Michael Damatov
la source
4
Autrement dit, les changements interverrouillés sont entièrement clôturés et en tant que tels sont atomiques. Les membres volatils ne sont que partiellement clôturés et en tant que tels ne sont pas garantis pour être thread-safe.
JoeGeeky
1
En fait, volatilene 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.)
David Schwartz
42

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

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 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 .

Zach Saw
la source
3
Intéressant. Pouvez-vous faire référence à cela? Je voterais avec plaisir, mais publier avec un langage agressif 3 ans après une réponse très votée conforme aux ressources que j'ai lues va nécessiter une preuve un peu plus tangible.
Steven Evers
Si vous pouvez indiquer la partie que vous souhaitez référencer, je serais heureux de trouver des informations quelque part (je doute fortement que j'aie divulgué des secrets commerciaux de fournisseurs x86 / x64, donc ils devraient être facilement disponibles sur wiki, Intel PRM (manuels de référence du programmeur), blogs MSFT, MSDN ou quelque chose de similaire) ...
Zach Saw
2
Pourquoi quelqu'un voudrait-il empêcher la mise en cache du CPU me dépasse? L'ensemble de l'immobilier (certainement non négligeable en taille et en coût) dédié à la cohérence du cache est complètement gaspillé si c'est le cas ... À moins que vous n'ayez besoin d'aucune cohérence de cache, comme une carte graphique, un périphérique PCI, etc., vous ne définiriez pas une ligne de cache à écrire.
Zach Saw
4
Oui, tout ce que vous dites est sinon 100% au moins 99% sur la marque. Ce site est (surtout) assez utile lorsque vous êtes en pleine phase de développement au travail mais malheureusement la précision des réponses correspondant au (jeu de) votes n'est pas là. Donc, fondamentalement, dans stackoverflow, vous pouvez avoir une idée de la compréhension populaire des lecteurs et non de ce qu'elle est vraiment. Parfois, les meilleures réponses ne sont que du charabia pur - des mythes du genre. Et malheureusement, c'est ce qui engendre les gens qui rencontrent la lecture tout en résolvant le problème. C'est compréhensible cependant, personne ne peut tout savoir.
user1416420
1
@BenVoigt Je pourrais continuer et répondre à propos de toutes les architectures sur lesquelles s'exécute .NET, mais cela prendrait quelques pages et n'est certainement pas adapté à SO. Il est de loin préférable d'éduquer les gens sur la base du modèle mem matériel sous-jacent .NET le plus largement utilisé que celui qui est arbitraire. Et avec mes commentaires «partout», je corrigeais les erreurs que les gens faisaient en supposant de vider / invalider le cache, etc. Ils ont fait des suppositions sur le matériel sous-jacent sans spécifier quel matériel.
Zach Saw
16

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

Lou Franco
la source
11

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.

Rob Walker
la source
8

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.

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
Kenneth Xu
la source
L'exemple de code que vous avez testé était pourtant si trivial - cela n'a vraiment aucun sens de le tester de cette façon! Le mieux serait de comprendre ce que font réellement les différentes méthodes et d'utiliser la méthode appropriée en fonction du scénario d'utilisation que vous avez.
Zach Saw
@Zach, la discussion sur le scénario d'augmentation d'un compteur de manière sécurisée pour les threads. Quel autre scénario d'utilisation pensiez-vous ou comment le testeriez-vous? Merci pour le commentaire BTW.
Kenneth Xu
Le fait est que c'est un test artificiel. Vous n'allez pas marteler le même endroit que souvent dans n'importe quel scénario du monde réel. Si vous l'êtes, alors vous êtes goulot d'étranglement par le FSB (comme indiqué dans vos boîtes de serveur). Quoi qu'il en soit, regardez ma réponse sur votre blog.
Zach Saw
2
En y repensant. Si le véritable goulot d'étranglement est avec FSB, l'implémentation du moniteur doit respecter le même goulot d'étranglement. La vraie différence est qu'Interlocked effectue une attente et une nouvelle tentative occupées, ce qui devient un vrai problème avec le comptage haute performance. J'espère au moins que mon commentaire attirera l'attention sur le fait qu'Interlocked n'est pas toujours le bon choix pour compter. Le fait que les gens recherchent des alternatives l'a bien expliqué. Vous avez besoin d'un long additionneur gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu
8

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)

zihotki
la source
3

Je voudrais ajouter mentionné dans les autres réponses à la différence entre volatile, Interlockedet lock:

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

  • Types de référence.
  • Types de pointeurs (dans un contexte dangereux). Notez que bien que le pointeur lui-même puisse être volatile, l'objet vers lequel il pointe ne le peut pas. En d'autres termes, vous ne pouvez pas déclarer un "pointeur" comme "volatile".
  • Les types simples tels que sbyte, byte, short, ushort, int, uint, char, floatet bool.
  • Un type enum avec l' un des types de base suivants: byte, sbyte, short, ushort, intou uint.
  • Paramètres de type générique connus pour être des types de référence.
  • IntPtret UIntPtr.

D'autres types , y compris doubleet long, 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 les Interlockedmembres de la classe ou protégez l'accès à l'aide de l' lockinstruction.

Vadim S.
la source