Question

J'ai été élevé dans l'idée que si plusieurs threads peuvent accéder à une variable, alors toutes les lectures et écritures dans cette variable doivent être protégées par un code de synchronisation, tel qu'une instruction "lock", car le processeur peut passer à un autre thread à mi-parcours. une écriture.

Cependant, je parcourais System.Web.Security.Membership à l'aide de Reflector et j'ai trouvé un code comme celui-ci :

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

Pourquoi le champ s_Initialized est-il lu en dehors du verrou ?Un autre thread ne pourrait-il pas essayer d'y écrire en même temps ? Les lectures et écritures de variables sont-elles atomiques ?

Était-ce utile?

La solution

Pour la réponse définitive, consultez les spécifications.:)

Partition I, section 12.6.6 de la spécification CLI indique :"Une CLI conforme doit garantir que l'accès en lecture et en écriture à des emplacements de mémoire correctement alignés ne dépassant pas la taille du mot natif est atomique lorsque tous les accès en écriture à un emplacement ont la même taille."

Cela confirme donc que s_Initialized ne sera jamais instable et que la lecture et l'écriture sur des types primitifs inférieurs à 32 bits sont atomiques.

En particulier, double et long (Int64 et UInt64) sont pas garanti atomique sur une plate-forme 32 bits.Vous pouvez utiliser les méthodes sur le Interlocked classe pour les protéger.

De plus, bien que les lectures et les écritures soient atomiques, il existe une condition de concurrence critique avec l'addition, la soustraction et l'incrémentation et la décrémentation de types primitifs, car ils doivent être lus, exploités et réécrits.La classe verrouillée vous permet de les protéger en utilisant le CompareExchange et Increment méthodes.

Le verrouillage crée une barrière de mémoire pour empêcher le processeur de réorganiser les lectures et les écritures.Le verrou crée la seule barrière requise dans cet exemple.

Autres conseils

Il s’agit d’une (mauvaise) forme du modèle de verrouillage à double contrôle qui est pas thread-safe en C# !

Il y a un gros problème dans ce code :

s_Initialized n'est pas volatile.Cela signifie que les écritures dans le code d'initialisation peuvent se déplacer une fois que s_Initialized est défini sur true et que les autres threads peuvent voir le code non initialisé même si s_Initialized est vrai pour eux.Cela ne s'applique pas à l'implémentation du Framework par Microsoft, car chaque écriture est une écriture volatile.

Mais également dans l'implémentation de Microsoft, les lectures des données non initialisées peuvent être réorganisées (c'est-à-direpréextraites par le processeur), donc si s_Initialized est vrai, la lecture des données qui doivent être initialisées peut entraîner la lecture d'anciennes données non initialisées en raison d'accès au cache (c.-à-d.les lectures sont réorganisées).

Par exemple:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

Déplacer la lecture de s_Provider avant la lecture de s_Initialized est parfaitement légal car il n'y a aucune lecture volatile nulle part.

Si s_Initialized était volatile, la lecture de s_Provider ne serait pas autorisée à se déplacer avant la lecture de s_Initialized et l'initialisation du fournisseur n'est pas non plus autorisée à se déplacer une fois que s_Initialized est défini sur true et tout va bien maintenant.

Joe Duffy a également écrit un article sur ce problème : Variantes cassées sur verrouillage revérifié

Attendez – la question qui est dans le titre n’est certainement pas la vraie question que pose Rory.

La question titulaire a la réponse simple « Non » – mais cela n’aide pas du tout, quand vous voyez la vraie question – à laquelle je ne pense pas que quiconque ait donné une réponse simple.

La vraie question que pose Rory est présentée beaucoup plus tard et est plus pertinente par rapport à l'exemple qu'il donne.

Pourquoi le champ S_Initialized est-il lu en dehors de la serrure?

La réponse à cette question est également simple, bien que totalement indépendante de l’atomicité de l’accès aux variables.

Le champ s_Initialized est lu en dehors du verrou car les serrures sont chères.

Étant donné que le champ s_Initialized est essentiellement « à écrire une fois », il ne renverra jamais de faux positif.

Il est économique de le lire en dehors de la serrure.

C'est un faible coût activité avec un haut chance d'avoir un avantage.

C'est pourquoi il est lu en dehors du verrou - pour éviter de payer le coût de l'utilisation d'un verrou à moins que cela ne soit indiqué.

Si les serrures étaient bon marché, le code serait plus simple et omettrait cette première vérification.

(modifier:une belle réponse de Rory suit.Ouais, les lectures booléennes sont très atomiques.Si quelqu'un construisait un processeur avec des lectures booléennes non atomiques, il figurerait sur le DailyWTF.)

La bonne réponse semble être : « Oui, la plupart du temps ».

  1. La réponse de John faisant référence à la spécification CLI indique que les accès aux variables ne dépassant pas 32 bits sur un processeur 32 bits sont atomiques.
  2. Confirmation supplémentaire de la spécification C#, section 5.5, Atomicité des références variables:

    Les lectures et écritures des types de données suivants sont atomiques :Types bool, char, byte, sbyte, short, ushort, uint, int, float et référence.De plus, les lectures et écritures de types enum avec un type sous-jacent dans la liste précédente sont également atomiques.Il n'est pas garanti que les lectures et écritures d'autres types, notamment long, ulong, double et décimal, ainsi que les types définis par l'utilisateur, soient atomiques.

  3. Le code de mon exemple a été paraphrasé à partir de la classe Membership, tel qu'écrit par l'équipe ASP.NET elle-même, il était donc toujours prudent de supposer que la façon dont il accède au champ s_Initialized est correcte.Maintenant, nous savons pourquoi.

Modifier:Comme le souligne Thomas Danecker, même si l'accès au champ est atomique, s_Initialized devrait bien être marqué volatil pour vous assurer que le verrouillage n'est pas rompu par le processeur réorganisant les lectures et les écritures.

La fonction Initialiser est défectueuse.Cela devrait ressembler davantage à ceci :

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

Sans le deuxième contrôle à l'intérieur de la serrure, il est possible que le code d'initialisation soit exécuté deux fois.Ainsi, la première vérification concerne les performances afin de vous éviter de verrouiller inutilement, et la deuxième vérification concerne le cas où un thread exécute le code d'initialisation mais n'a pas encore défini le s_Initialized flag et donc un deuxième thread passerait la première vérification et attendrait au verrou.

Les lectures et écritures de variables ne sont pas atomiques.Vous devez utiliser les API de synchronisation pour émuler les lectures/écritures atomiques.

Pour une référence géniale sur ce sujet et sur bien d'autres problèmes liés à la concurrence, assurez-vous de vous procurer une copie de Joe Duffy's dernier spectacle.C'est un éventreur !

« L'accès à une variable en C# est-il une opération atomique ?

Non.Et ce n'est pas une affaire de C#, ni même une affaire de .net, c'est une affaire de processeur.

OJ a raison : Joe Duffy est la personne à qui s'adresser pour ce genre d'informations.ET « verrouillé » est un excellent terme de recherche à utiliser si vous souhaitez en savoir plus.

Des « lectures déchirées » peuvent se produire sur n’importe quelle valeur dont la somme des champs dépasse la taille d’un pointeur.

@Léon
Je comprends votre point de vue - la façon dont j'ai posé la question, puis commenté, la question permet de la prendre de différentes manières.

Pour être clair, je voulais savoir s'il était sûr que des threads simultanés lisent et écrivent dans un champ booléen sans aucun code de synchronisation explicite, c'est-à-dire qu'ils accèdent à une variable atomique booléenne (ou autre type primitif).

J'ai ensuite utilisé le code d'adhésion pour donner un exemple concret, mais cela a introduit de nombreuses distractions, comme le verrouillage à double vérification, le fait que s_Initialized n'est défini qu'une seule fois et que j'ai commenté le code d'initialisation lui-même.

Ma faute.

Vous pouvez également décorer s_Initialized avec le mot-clé volatile et renoncer complètement à l'utilisation du lock.

Ce n'est pas correct.Vous rencontrerez toujours le problème d'un deuxième thread réussissant la vérification avant que le premier thread n'ait eu la chance de définir l'indicateur, ce qui entraînera plusieurs exécutions du code d'initialisation.

Je pense que tu demandes si s_Initialized pourrait être dans un état instable lorsqu’il est lu en dehors du verrou.La réponse courte est non.Une simple affectation/lecture se résumera à une seule instruction d'assemblage qui est atomique sur chaque processeur auquel je peux penser.

Je ne suis pas sûr de l'utilité de l'affectation à des variables 64 bits, cela dépend du processeur, je suppose que ce n'est pas atomique mais c'est probablement le cas sur les processeurs 32 bits modernes et certainement sur tous les processeurs 64 bits.L’attribution de types de valeurs complexes ne sera pas atomique.

Je pensais qu'ils l'étaient - je ne suis pas sûr de l'intérêt du verrou dans votre exemple, à moins que vous ne fassiez également quelque chose à s_Provider en même temps - alors le verrou garantirait que ces appels se produisent ensemble.

Est-ce que //Perform initialization couverture de commentaire créant s_Provider ?Par exemple

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Sinon, cette propriété statique va retourner null de toute façon.

Peut-être Verrouillé donne un indice.Et sinon celui-ci je suis plutôt bien.

J'aurais deviné que ce n'était pas atomique.

Pour que votre code fonctionne toujours sur des architectures faiblement ordonnées, vous devez mettre un MemoryBarrier avant d'écrire s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

Les écritures en mémoire qui se produisent dans le constructeur MembershipProvider et l'écriture dans s_Provider ne sont pas garanties avant que vous écriviez dans s_Initialized sur un processeur faiblement ordonné.

Une grande partie de la réflexion dans ce fil porte sur la question de savoir si quelque chose est atomique ou non.Ce n'est pas le problème.Le problème est l'ordre dans lequel les écritures de votre fil sont visibles par les autres fils.Sur les architectures faiblement ordonnées, les écritures en mémoire ne se produisent pas dans l'ordre et CELA est le vrai problème, pas celui de savoir si une variable s'intègre dans le bus de données.

MODIFIER: En fait, je mélange les plateformes dans mes déclarations.En C#, la spécification CLR exige que les écritures soient globalement visibles, dans l'ordre (en utilisant des instructions de magasin coûteuses pour chaque magasin si nécessaire).Par conséquent, vous n’avez pas besoin d’avoir cette barrière de mémoire.Cependant, s'il s'agissait de C ou de C++ où aucune garantie d'ordre de visibilité globale n'existe et que votre plate-forme cible peut avoir une mémoire faiblement ordonnée et qu'elle est multithread, vous devrez alors vous assurer que les écritures des constructeurs sont globalement visibles avant de mettre à jour s_Initialized. , qui est testé à l'extérieur de la serrure.

Un If (itisso) { Vérifier un booléen est atomique, mais même s'il n'était pas nécessaire de verrouiller le premier chèque.

Si un thread a terminé l’initialisation, ce sera vrai.Cela n'a pas d'importance si plusieurs threads vérifient en même temps.Ils obtiendront tous la même réponse et il n’y aura pas de conflit.

La deuxième vérification à l'intérieur du verrou est nécessaire car un autre thread peut avoir saisi le verrou en premier et déjà terminé le processus d'initialisation.

Ce que vous demandez, c'est si l'accès à un champ dans une méthode plusieurs fois est atomique - ce à quoi la réponse est non.

Dans l'exemple ci-dessus, la routine d'initialisation est défectueuse car elle peut entraîner plusieurs initialisations.Il faudrait vérifier le s_Initialized drapeau à l'intérieur du verrou ainsi qu'à l'extérieur, pour éviter une condition de concurrence critique dans laquelle plusieurs threads lisent le s_Initialized flag avant que l'un d'eux ne fasse réellement le code d'initialisation.Par exemple.,

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Ack, tant pis...comme indiqué, c'est effectivement incorrect.Cela n'empêche pas un deuxième thread d'entrer dans la section de code "initialiser".Bah.

Vous pouvez également décorer s_Initialized avec le mot-clé volatile et renoncer complètement à l'utilisation du lock.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top