Quand utiliser le mot clé volatile en C #

Les techniques d'optimisation utilisées par le compilateur JIT (juste à temps) dans le Common Language Runtime peuvent conduire à des résultats imprévisibles lorsque votre programme .Net tente d'effectuer des lectures non volatiles de données dans un scénario multithread. Dans cet article, nous examinerons les différences entre l'accès à la mémoire volatile et non volatile, le rôle du mot clé volatile en C # et la manière dont le mot clé volatile doit être utilisé.

Je vais fournir quelques exemples de code en C # pour illustrer les concepts. Pour comprendre le fonctionnement du mot-clé volatile, nous devons d'abord comprendre comment la stratégie d'optimisation du compilateur JIT fonctionne dans .Net.

Comprendre les optimisations du compilateur JIT

Il convient de noter que le compilateur JIT va, dans le cadre d'une stratégie d'optimisation, changer l'ordre des lectures et des écritures d'une manière qui ne change pas la signification et la sortie éventuelle du programme. Ceci est illustré dans l'extrait de code ci-dessous.

x = 0;

x = 1;

L'extrait de code ci-dessus peut être modifié comme suit, tout en préservant la sémantique d'origine du programme.

x = 1;

Le compilateur JIT peut également appliquer un concept appelé «propagation constante» pour optimiser le code suivant.

x = 1;

y = x;

L'extrait de code ci-dessus peut être modifié comme suit - encore une fois tout en préservant la sémantique d'origine du programme.

x = 1;

y = 1;

Accès mémoire volatile vs non volatile

Le modèle de mémoire des systèmes modernes est assez compliqué. Vous avez des registres de processeur, différents niveaux de caches et la mémoire principale partagée par plusieurs processeurs. Lorsque votre programme s'exécute, le processeur peut mettre en cache les données puis accéder à ces données à partir du cache lorsque cela est demandé par le thread en cours d'exécution. Les mises à jour et les lectures de ces données peuvent s'exécuter sur la version mise en cache des données, tandis que la mémoire principale est mise à jour ultérieurement. Ce modèle d'utilisation de la mémoire a des conséquences pour les applications multithread. 

Lorsqu'un thread interagit avec les données du cache et qu'un deuxième thread essaie de lire les mêmes données simultanément, le deuxième thread peut lire une version obsolète des données à partir de la mémoire principale. En effet, lorsque la valeur d'un objet non volatile est mise à jour, la modification est effectuée dans le cache du thread en cours d'exécution et non dans la mémoire principale. Cependant, lorsque la valeur d'un objet volatil est mise à jour, non seulement la modification est effectuée dans le cache du thread en cours d'exécution, mais ce cache est ensuite vidé dans la mémoire principale. Et lorsque la valeur d'un objet volatile est lue, le thread actualise son cache et lit la valeur mise à jour.

Utilisation du mot clé volatile en C #

Le mot clé volatile en C # est utilisé pour informer le compilateur JIT que la valeur de la variable ne doit jamais être mise en cache car elle peut être modifiée par le système d'exploitation, le matériel ou un thread s'exécutant simultanément. Le compilateur évite ainsi d'utiliser des optimisations sur la variable qui pourraient conduire à des conflits de données, c'est-à-dire à différents threads accédant à différentes valeurs de la variable.

Lorsque vous marquez un objet ou une variable comme volatile, il devient un candidat pour les lectures et écritures volatiles. Il convient de noter qu'en C #, toutes les écritures en mémoire sont volatiles, que vous écriviez des données sur un objet volatil ou non volatile. Cependant, l'ambiguïté se produit lorsque vous lisez des données. Lorsque vous lisez des données non volatiles, le thread en cours d'exécution peut ou ne peut pas toujours obtenir la dernière valeur. Si l'objet est volatil, le thread obtient toujours la valeur la plus à jour.

Vous pouvez déclarer une variable comme volatile en la précédant du volatilemot clé. L'extrait de code suivant illustre cela.

programme de classe

    {

        public volatile int i;

        static void Main (string [] args)

        {

            // Écrivez votre code ici

        }

    }

Vous pouvez utiliser le volatilemot - clé avec n'importe quel type de référence, pointeur et énumération. Vous pouvez également utiliser le modificateur volatile avec les types byte, short, int, char, float et bool. Il convient de noter que les variables locales ne peuvent pas être déclarées comme volatiles. Lorsque vous spécifiez un objet de type référence comme volatile, seul le pointeur (un entier 32 bits qui pointe vers l'emplacement en mémoire où l'objet est réellement stocké) est volatil, pas la valeur de l'instance. De plus, une variable double ne peut pas être volatile car sa taille est de 64 bits, plus grande que la taille du mot sur les systèmes x86. Si vous avez besoin de rendre volatile une variable double, vous devez l'envelopper dans la classe. Vous pouvez le faire facilement en créant une classe wrapper comme indiqué dans l'extrait de code ci-dessous.

classe publique VolatileDoubleDemo

{

    private volatile WrappedVolatileDouble volatileData;

}

public class WrappedVolatileDouble

{

    public double Data {get; ensemble; }

Cependant, notez la limitation de l'exemple de code ci-dessus. Bien que vous disposiez de la dernière valeur du volatileDatapointeur de référence, vous n'êtes pas assuré de la dernière valeur de la Datapropriété. Le contournement pour cela consiste à rendre le WrappedVolatileDoubletype immuable.

Bien que le mot clé volatile puisse vous aider dans la sécurité des threads dans certaines situations, ce n'est pas une solution à tous vos problèmes de concurrence des threads. Vous devez savoir que marquer une variable ou un objet comme volatile ne signifie pas que vous n'avez pas besoin d'utiliser le mot-clé lock. Le mot clé volatile ne remplace pas le mot clé lock. Il n'est là que pour vous aider à éviter les conflits de données lorsque plusieurs threads tentent d'accéder aux mêmes données.