web-dev-qa-db-fra.com

c # - Utilisation volatile des mots clés vs verrouillage

J'ai utilisé du volatile là où je ne suis pas sûr que ce soit nécessaire. J'étais presque sûr qu'un verrou serait exagéré dans ma situation. La lecture de ce fil (commentaire d'Eric Lippert) me rend anxieux sur mon utilisation de volatile: Quand le mot-clé volatile doit-il être utilisé en c #?

J'ai utilisé volatile parce que ma variable est utilisée dans un contexte multithread où cette variable peut être accédée/modifiée simultanément, mais où je peux perdre un ajout sans aucun mal (voir code).

J'ai ajouté "volatile" pour m'assurer qu'il n'y a pas d'alignement manquant: lire seulement 32 bits de la variable et les 32 autres bits sur un autre fetch qui peut être cassé en 2 par une écriture au milieu d'un autre thread.

Est-ce que mon hypothèse précédente (déclaration précédente) peut vraiment arriver ou non? Sinon, une utilisation "volatile" est-elle toujours nécessaire (des modifications des propriétés des options peuvent se produire dans n'importe quel thread).

Après avoir lu les 2 premières réponses. Je voudrais insister sur le fait que la façon dont le code est écrit, ce n'est pas important si en raison de la concurrence, nous manquons un incrément (nous voulons incrémenter à partir de 2 threads mais le résultat n'est incrémenté que d'un en raison de la concurrence) si au moins la variable '_actualVersion' est incrémentée.

Comme référence, c'est la partie du code où je l'utilise. Il s'agit de signaler une action de sauvegarde (écriture sur disque) uniquement lorsque l'application est inactive.

public abstract class OptionsBase : NotifyPropertyChangedBase
{
    private string _path;

    volatile private int _savedVersion = 0;
    volatile private int _actualVersion = 0;

    // ******************************************************************
    void OptionsBase_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        _actualVersion++;
        Application.Current.Dispatcher.BeginInvoke(new Action(InternalSave), DispatcherPriority.ApplicationIdle);
    }

    // ******************************************************************
    private void InternalSave()
    {
        if (_actualVersion != _savedVersion)
        {
            _savedVersion = _actualVersion;
            Save();
        }
    }

    // ******************************************************************
    /// <summary>
    /// Save Options
    /// </summary>
    private void Save()
    {
        using (XmlTextWriter writer = new XmlTextWriter(_path, null))
        {
            writer.Formatting = Formatting.Indented;
            XmlSerializer x = new XmlSerializer(this.GetType());

            x.Serialize(writer, this);
            writer.Close();
        }
    }
30
Eric Ouellet

J'ai utilisé des produits volatils là où je ne suis pas sûr que ce soit nécessaire.

Permettez-moi d'être très clair sur ce point:

Si vous n'êtes pas sûr à 100% de ce que signifie volatile en C # alors ne l'utilisez pas . C'est un outil pointu qui est destiné à être utilisé uniquement par des experts. Si vous ne pouvez pas décrire ce que tous les réordonnancements possibles des accès à la mémoire sont autorisés par une architecture de modèle de mémoire faible lorsque deux threads lisent et écrivent deux champs volatils différents, alors vous ne savez pas assez pour utiliser volatile en toute sécurité et vous ferez des erreurs, comme vous l'avez fait fait ici, et écrivez un programme qui est extrêmement fragile.

J'étais presque sûr qu'une serrure serait exagérée dans ma situation

Tout d'abord, la meilleure solution est de simplement ne pas y aller. Si vous n'écrivez pas de code multithread qui essaie de partager la mémoire, vous n'avez pas à vous soucier du verrouillage, ce qui est difficile à corriger.

Si vous devez écrire du code multithread qui partage la mémoire, la meilleure pratique consiste à toujours utiliser des verrous. Les serrures ne sont presque jamais exagérées. Le prix d'un verrou incontrôlé est de l'ordre de dix nanosecondes. Êtes-vous vraiment en train de me dire que dix nanosecondes supplémentaires feront une différence pour votre utilisateur? Si oui, alors vous avez un programme très, très rapide et un utilisateur avec des normes inhabituellement élevées.

Le prix d'une serrure contestée est bien sûr arbitrairement élevé si le code à l'intérieur de la serrure est cher. Ne faites pas de travail coûteux à l'intérieur d'une serrure, de sorte que la probabilité de contention est faible.

Ce n'est que lorsque vous avez un problème de performances démontré avec des verrous qui ne peuvent pas être résolus en supprimant les conflits que vous devriez même commencer à envisager une solution à faible verrouillage.

J'ai ajouté "volatile" pour m'assurer qu'il n'y a pas de désalignement: lire seulement 32 bits de la variable et les 32 autres bits sur un autre fetch qui peut être cassé en deux par une écriture au milieu d'un autre thread.

Cette phrase me dit que vous devez arrêter d'écrire du code multithread dès maintenant. Le code multithread, en particulier le code low-lock, est réservé aux experts. Vous devez comprendre le fonctionnement réel du système avant de recommencer à écrire du code multithread. Obtenez un bon livre sur le sujet et étudiez dur.

Votre phrase est absurde parce que:

Tout d'abord, les entiers ne sont déjà que 32 bits.

Deuxièmement, les accès int sont garantis par la spécification comme étant atomiques! Si vous voulez l'atomicité, vous l'avez déjà.

Troisièmement, oui, il est vrai que les accès volatils sont toujours atomiques, mais ce n'est pas parce que C # transforme tous les accès volatils en accès atomiques! Au contraire, C # rend illégal de mettre volatile sur un champ sauf si le champ est déjà atomique.

Quatrièmement, le but de volatile est d'empêcher le compilateur C #, la gigue et le CPU de faire certaines optimisations qui changeraient la signification de votre programme dans un modèle de mémoire faible. La volatilité en particulier ne rend pas ++ atomique. (Je travaille pour une entreprise qui fabrique des analyseurs statiques; j'utiliserai votre code comme cas de test pour notre vérificateur "Opération non atomique incorrecte sur champ volatil". Il m'est très utile d'obtenir un code du monde réel plein de erreurs réalistes; nous voulons nous assurer que nous trouvons réellement les bogues que les gens écrivent, alors merci d'avoir posté cela.)

En regardant votre code actuel: volatile est, comme l'a souligné Hans, totalement insuffisant pour rendre votre code correct. La meilleure chose à faire est ce que j'ai dit auparavant: ne pas autoriser ces méthodes à être appelées sur un thread autre que le thread principal. Que la contre-logique soit fausse devrait être le moindre de vos soucis. Qu'est-ce qui rend le thread de sérialisation sûr si du code sur un autre thread modifie les champs de l'objet pendant sa sérialisation? C'est le problème qui devrait vous inquiéter en premier.

63
Eric Lippert

Volatile est malheureusement insuffisant pour sécuriser ce code. Vous pouvez utiliser le verrouillage de bas niveau avec Interlocked.Increment () et Interlocked.CompareExchange () mais il y a très peu de raisons de supposer que Save () est thread-safe. Il semble bien qu'il essaie d'enregistrer un objet en cours de modification par un thread de travail.

L'utilisation de lock est très fortement indiquée ici, non seulement pour protéger les numéros de version mais également pour empêcher l'objet de changer pendant sa sérialisation. Les sauvegardes corrompues que vous obtiendrez en ne faisant pas cela sont tout à fait trop rares pour avoir une chance de déboguer le problème.

13
Hans Passant

Selon l'excellent --- de Joe Albahari publication sur les threads et les verrous qui est de son livre tout aussi excellent C # 5.0 In A Nutshell, il dit ici que même lorsque le mot-clé volatile est utilisé, que une instruction d'écriture suivie d'une instruction de lecture peut être réorganisée.

Plus bas, il dit que la documentation MSDN sur ce sujet est incorrecte et suggère qu'il y a de fortes raisons d'être pour éviter complètement le mot-clé volatile . Il souligne que même si vous comprenez les subtilités impliquées, les autres développeurs en aval le comprendront-ils également?

Ainsi, l'utilisation d'un verrou n'est pas seulement plus "correcte", elle est également plus compréhensible, offre la possibilité d'ajouter une nouvelle fonctionnalité qui ajoute plusieurs instructions de mise à jour au code de manière atomique - qui n'est ni volatile ni une classe de clôture comme MemoryBarrier peut le faire, est très rapide et est beaucoup plus facile à maintenir car il y a beaucoup moins de chances d'introduire un bug subtil par des développeurs moins expérimentés.

3
Bob Bryan

En ce qui concerne votre déclaration si une variable peut être divisée en deux récupérations 32 bits lors de l'utilisation de volatile, cela pourrait être une possibilité si vous utilisiez quelque chose de plus grand que Int32.

Donc, tant que vous utilisez Int32, vous n'avez pas de problème avec ce que vous déclarez.

Cependant, comme vous l'avez lu dans le lien suggéré, volatile ne vous donne que de faibles garanties et je préférerais que les verrous soient du bon côté, car les machines d'aujourd'hui ont plus d'un CPU et volatile ne garantit pas qu'un autre CPU ne balaye pas. et faire quelque chose d'inattendu.

MODIFIER

Avez-vous envisagé d'utiliser Interlocked.Increment?

1
idipous

La comparaison et l'affectation dans votre méthode InternalSave () ne seraient pas thread-safe avec ou sans le mot-clé volatile. Si vous souhaitez éviter d'utiliser des verrous, vous pouvez utiliser les méthodes CompareExchange () et Increment () dans votre code de la classe Interlocked du framework.

0
user2847824