web-dev-qa-db-fra.com

Volatile vs. Interlocked vs. Lock

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);,
  • Remplacez le modificateur d'accès de 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?

638
core

Pire (ne fonctionnera pas réellement)

Remplacez le modificateur d'accès de counter par public 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.

Deuxième meilleur:

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.

Meilleur

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:

  1. LES MÉTHODES VERROUILLÉES SONT SIMPLEMENT SÉCURITAIRES SUR TOUT NOMBRE DE CŒURS OR CPU.
  2. Les méthodes imbriquées appliquent une clôture complète autour des instructions qu'elles exécutent. Par conséquent, le réordonnancement ne se produit pas.
  3. Méthodes imbriquées ne nécessite pas ou même ne supporte pas l'accès à un champ volatile, comme volatile est placé une demi-clôture autour d'opérations sur un champ donné et que la clé complète est verrouillée.

Note de bas de page: Ce qui est volatile est vraiment bon pour.

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.

825
Orion Edwards

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.

137
Jon Skeet

"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:

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

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

42
Michael Damatov

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 .

41
Zach Saw

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

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

15
Lou Franco

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.

11
Rob Walker
8
zihotki

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
7
Kenneth Xu

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 :

  • 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 sur lequel il pointe ne le peut pas. En d'autres termes, vous ne pouvez pas déclarer un "pointeur" comme étant "volatil".
  • Types simples tels que sbyte, byte, short, ushort, int, uint, char, float et bool.
  • Un type enum avec l'un des types de base suivants: byte, sbyte, short, ushort, int ou uint.
  • Paramètres de type génériques connus pour être des types de référence.
  • 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.

2
Vadim S.