94 votes

La nécessité pour la volatilité du modificateur dans une double vérification de verrouillage .NET

Plusieurs textes disent que lors de l'application d'un double contrôle de verrouillage .NET le domaine vous êtes le verrouillage doit avoir volatils modificateur appliqué. Mais pourquoi exactement? En considérant l'exemple suivant:

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

pourquoi ne pas "verrouiller (syncRoot)" réaliser la nécessaire consistance de la mémoire? N'est-il pas vrai que, après "lock" déclaration de lire et d'écrire serait instable et donc la nécessaire cohérence serait accompli?

63voto

dan Points 4107

Volatile est inutile. Eh bien, sorte de**

volatile est utilisé pour créer une barrière de mémoire* entre les lectures et les écritures sur la variable.
lock, lorsqu'il est utilisé, les causes de la mémoire obstacles à l'être créé autour du bloc à l'intérieur de l' lock, en plus de limiter l'accès au bloc à un seul thread.
Les barrières de la mémoire faire en sorte que chaque thread lit le plus de valeur actuelle de la variable (et non une valeur locale mis en cache dans certains de registre) et que le compilateur ne pas réorganiser consolidés. À l'aide de volatile est inutile** parce que vous avez déjà obtenu un verrouillage.

Joseph Albahari explique ça bien mieux que je ne pourrais le faire.

Et assurez-vous de vérifier Jon Skeet du guide de mise en œuvre le singleton en C#


mise à jour:
*volatile causes lit de la variable VolatileReads et écrit pour être VolatileWrites, qui, sur x86 et x64 sur CLR, sont mis en œuvre avec un MemoryBarrier. Ils peuvent être plus fine sur d'autres systèmes.

**ma réponse est correcte uniquement si vous utilisez le CLR sur les processeurs x86 et x64. Il pourrait être vrai dans d'autres modèles de mémoire, comme sur Mono (et d'autres implémentations), Itanium64 et avenir matériel. C'est ce que Jon se réfère dans son article sur les "pièges" pour une double vérification de verrouillage.

Procédant de l'une des {marquant la variable volatile, en le lisant, avec Thread.VolatileRead, ou de l'insertion d'un appel à l' Thread.MemoryBarrier} peut être nécessaire pour que le code fonctionne correctement dans un modèle de mémoire faible situation.

Ce que je comprends, sur le CLR (même sur IA64), les écritures ne sont jamais réorganisées (écrit toujours la libération de la sémantique). Cependant, sur IA64, lectures peuvent être réorganisées à venir avant l'écrit, sauf s'ils sont marqués volatile. Malheureusement, je n'ai pas accès à IA64 matériel de jouer avec, donc tout ce que je dis à ce sujet serait de la spéculation.

j'ai trouvé aussi ces articles utiles:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison de l'article (tout les liens de ce, il parle d'une double vérification de verrouillage)
chris brumme de l'article (tout les liens vers ce)
Joe Duffy: Les Variantes de la Double vérification de Verrouillage

luis abreu série sur le multithreading donne un bon aperçu des concepts trop
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

36voto

Miguel Angelo Points 12471

Il y a un moyen de la mettre en œuvre sans volatile champ. Je vais l'expliquer...

Je pense que c'est l'accès à la mémoire de réorganisation à l'intérieur de la serrure qui est dangereux, tels que vous pouvez obtenir un pas completelly initialisé instance à l'extérieur de la serrure. Pour éviter cela, je fais ceci:

public sealed class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         // very fast test, without implicit memory barriers or locks
         if (instance == null)
         {
            lock (syncRoot)
            {
               if (instance == null)
               {
                    var temp = new Singleton();

                    // ensures that the instance is well initialized,
                    // and only then, it assigns the static variable.
                    System.Threading.Thread.MemoryBarrier();
                    instance = temp;
               }
            }
         }

         return instance;
      }
   }
}

Comprendre le code

Imaginez qu'il y a quelques code d'initialisation dans le constructeur de la classe Singleton. Si ces instructions sont réorganisées après le champ est défini par l'adresse de l'objet nouveau, alors vous avez une instance incomplet... imaginez que la classe a ce code:

private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}

Maintenant, imaginez un appel au constructeur de la classe à l'aide de l'opérateur new.

instance = new Singleton();

Elle peut être élargie à ces opérations:

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

Que faire si je réorganiser ces instructions comme ceci:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

Fait-il une différence? PAS si vous pensez que d'un seul thread. OUI , si vous pensez à de multiples threads... que si le thread est interruped juste après l' set instance to ptr:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized

Qu'est ce que la barrière de la mémoire évite, en ne permettant pas l'accès à la mémoire de réorganisation:

ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;

Amusez-vous bien!

7voto

Jason Williams Points 31901

Je ne pense pas que quelqu'un a réellement répondu à la question, donc je vais lui donner un essai.

De la volatilité et de la première if (instance == null) ne sont pas "nécessaires". La serrure se rendre ce code thread-safe.

La question est donc: pourquoi voudriez-vous ajouter le premier if (instance == null)?

La raison en est sans doute pour éviter l'exécution de l'verrouillé section de code inutilement. Pendant que vous exécutez le code à l'intérieur de la serrure, aucun autre thread qui tente également d'exécuter ce code est bloqué, ce qui va ralentir votre programme vers le bas si vous essayez d'accéder à l'singleton fréquemment à partir de plusieurs threads. En fonction de la langue/plate-forme, il pourrait également y avoir des frais généraux de la serrure en elle-même que vous voulez éviter.

Donc, la première null check est ajouté comme un moyen rapide de voir si vous avez besoin de la serrure. Si vous n'avez pas besoin de créer le singleton, vous pouvez éviter le verrouillage entièrement.

Mais vous ne pouvez pas vérifier si la référence est nulle sans verrouillage d'une certaine façon, parce qu'en raison de la mise en cache du processeur, un autre thread pourrait changer et que vous lisez un "vicié" valeur qui pourraient vous amener à rentrer dans la serrure inutilement. Mais vous êtes en essayant d'éviter un lock!

Donc, vous faites le singleton volatile pour s'assurer que vous avez lu la dernière valeur, sans avoir à utiliser un verrou.

Vous devez toujours l'intérieure de verrouillage car volatils seulement vous protège lors d'un accès unique à la variable, vous pouvez pas test-and-set en toute sécurité sans l'aide d'un verrou.

Maintenant, est-ce vraiment utile?

Eh bien je dirais que "dans la plupart des cas, aucune".

Si Singleton.Exemple, pourraient entraîner l'inefficacité due à l'serrures, alors pourquoi êtes-vous de l'appeler si souvent que cela serait un problème important? Le point de l'ensemble d'un singleton est qu'il y est un seul, de sorte que votre code peut lire et cache le singleton de référence une fois.

Le seul cas que je peux penser d'où cette mise en cache ne serait pas possible lorsque vous avez un grand nombre de threads (par exemple, un serveur à l'aide d'un nouveau thread pour traiter chaque demande pourrait être de créer des millions de très court threads en cours d'exécution, chaque de ce qui aurait pu faire appel de Singleton.Exemple une fois).

Donc je pense qu'une double vérification de verrouillage est un mécanisme qui a une vraie place dans la très précis de la performance-les cas critiques, et puis tout le monde a grimpé sur le "c'est la bonne façon de le faire" boule de neige, sans vraiment penser à ce qu'il fait et s'il va être nécessaire dans le cas où ils utilisent.

3voto

Benjamin Podszun Points 5120

Autant que je sache (et attention, je ne fais pas beaucoup de concurrentes des trucs) no. La serrure vous donne juste la synchronisation entre plusieurs prétendants (threads).

volatils, d'autre part, vous indiquez à votre machine à réévaluer la valeur de tous les temps, de sorte que vous ne pas tomber sur une mise en cache (et le mal).

Voir http://msdn.microsoft.com/en-us/library/ms998558.aspx et note la citation suivante:

Aussi, la variable est déclarée à être volatile pour s'assurer que l'affectation à la variable d'instance est terminée avant la variable d'instance peut être consulté.

Une description de la volatilité: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

2voto

Marc Gravell Points 482669

L' lock est suffisante. La MME langue spec (3.0) lui-même mentionne exacte de ce scénario dans le §8.12, sans aucune mention de l' volatile:

Une meilleure approche consiste à synchroniser l'accès à des données statiques par le verrouillage d'un privé d'objet statique. Par exemple:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
  		...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
  		...
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X