l'attribution de référence est atomique alors pourquoi Interlocked.Exchange (ref Object, Object) est-il nécessaire?

108

Dans mon service Web asmx multithread, j'avais un champ de classe _allData de mon propre type SystemData qui se compose de quelques List<T>- uns et Dictionary<T>marqué comme volatile. Les données système ( _allData) sont actualisées de temps en temps et je le fais en créant un autre objet appelé newDataet en remplissant ses structures de données avec de nouvelles données. Quand c'est fait je viens d'assigner

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Cela devrait fonctionner car l'affectation est atomique et les threads qui ont la référence aux anciennes données continuent de l'utiliser et les autres ont les nouvelles données système juste après l'affectation. Cependant, mon collègue a dit qu'au lieu d'utiliser des volatilemots clés et une simple attribution, je devrais utiliser InterLocked.Exchangeparce qu'il a dit que sur certaines plates-formes, il n'est pas garanti que l'attribution de référence soit atomique. De plus: quand je déclare the _allDataterrain volatilela

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produit un avertissement "une référence à un champ volatil ne sera pas considérée comme volatile" Que dois-je en penser?

charme
la source

Réponses:

180

Il y a de nombreuses questions ici. En les considérant un à la fois:

l'attribution de référence est atomique alors pourquoi Interlocked.Exchange (ref Object, Object) est-il nécessaire?

L'attribution de référence est atomique. Interlocked.Exchange ne fait pas que référence à l'affectation. Il lit la valeur actuelle d'une variable, cache l'ancienne valeur et assigne la nouvelle valeur à la variable, le tout comme une opération atomique.

mon collègue a dit que sur certaines plates-formes, il n'est pas garanti que l'attribution des références soit atomique. Mon collègue avait-il raison?

L'affectation de référence est garantie comme atomique sur toutes les plates-formes .NET.

Mon collègue raisonne à partir de fausses prémisses. Cela signifie-t-il que leurs conclusions sont incorrectes?

Pas nécessairement. Votre collègue pourrait vous donner de bons conseils pour de mauvaises raisons. Il y a peut-être une autre raison pour laquelle vous devriez utiliser Interlocked.Exchange. La programmation sans verrouillage est incroyablement difficile et au moment où vous vous écartez des pratiques bien établies adoptées par des experts dans le domaine, vous êtes dans les mauvaises herbes et vous risquez les pires conditions de course. Je ne suis ni un expert dans ce domaine ni un expert de votre code, je ne peux donc pas porter de jugement dans un sens ou dans l'autre.

produit un avertissement "une référence à un champ volatil ne sera pas considérée comme volatile" Que dois-je en penser?

Vous devez comprendre pourquoi c'est un problème en général. Cela permettra de comprendre pourquoi l'avertissement est sans importance dans ce cas particulier.

La raison pour laquelle le compilateur donne cet avertissement est que le fait de marquer un champ comme volatile signifie "ce champ va être mis à jour sur plusieurs threads - ne générez aucun code qui met en cache les valeurs de ce champ, et assurez-vous que toute lecture ou écriture de ce champ n'est pas "déplacé vers l'avant et vers l'arrière dans le temps" via les incohérences du cache du processeur. "

(Je suppose que vous comprenez déjà tout cela. Si vous n'avez pas une compréhension détaillée de la signification de volatile et de son impact sur la sémantique du cache du processeur, vous ne comprenez pas comment cela fonctionne et ne devriez pas utiliser volatile. Programmes sans verrouillage sont très difficiles à réaliser; assurez-vous que votre programme est correct parce que vous comprenez comment il fonctionne, et non par accident.)

Supposons maintenant que vous créez une variable qui est un alias d'un champ volatil en passant une référence à ce champ. Dans la méthode appelée, le compilateur n'a aucune raison de savoir que la référence doit avoir une sémantique volatile! Le compilateur générera joyeusement du code pour la méthode qui ne parvient pas à implémenter les règles pour les champs volatils, mais la variable est un champ volatil. Cela peut complètement détruire votre logique sans verrouillage; l'hypothèse est toujours qu'un champ volatil est toujours accessible avec une sémantique volatile. Cela n'a aucun sens de le traiter comme volatil parfois et pas à d'autres moments; tu dois toujours être cohérent sinon vous ne pouvez pas garantir la cohérence sur les autres accès.

Par conséquent, le compilateur vous avertit lorsque vous faites cela, car il va probablement complètement gâcher votre logique sans verrouillage soigneusement développée.

Bien sûr, Interlocked.Exchange est écrit pour s'attendre à un champ volatil et faire ce qu'il faut. L'avertissement est donc trompeur. Je le regrette beaucoup; ce que nous aurions dû faire est d'implémenter un mécanisme par lequel un auteur d'une méthode comme Interlocked.Exchange pourrait mettre un attribut sur la méthode en disant "cette méthode qui prend une référence applique une sémantique volatile sur la variable, donc supprimez l'avertissement". Peut-être que dans une prochaine version du compilateur nous le ferons.

Eric Lippert
la source
1
D'après ce que j'ai entendu, Interlocked.Exchange garantit également qu'une barrière de mémoire est créée. Donc, si vous créez un nouvel objet, par exemple, attribuez quelques propriétés, puis stockez l'objet dans une autre référence sans utiliser Interlocked.Exchange, le compilateur peut perturber l'ordre de ces opérations, rendant ainsi l'accès à la deuxième référence non à thread- sûr. Est-ce vraiment le cas? Est-il judicieux d'utiliser Interlocked.Exchange est ce genre de scénarios?
Mike
12
@Mike: Quand il s'agit de ce qui est peut-être observé dans des situations multithread low-lock, je suis aussi ignorant que le prochain. La réponse variera probablement d'un processeur à l'autre. Vous devriez adresser votre question à un expert, ou lire sur le sujet si cela vous intéresse. Le livre de Joe Duffy et son blog sont de bons points de départ. Ma règle: n'utilisez pas le multithreading. Si vous le devez, utilisez des structures de données immuables. Si vous ne pouvez pas, utilisez des verrous. Ce n'est que lorsque vous devez avoir des données mutables sans verrous que vous devriez envisager des techniques de verrouillage faible.
Eric Lippert
Merci pour votre réponse Eric. Cela m'intéresse en effet, c'est pourquoi j'ai lu des livres et des blogs sur les stratégies de multithreading et de verrouillage et j'ai également essayé de les implémenter dans mon code. Mais il y a encore beaucoup à apprendre ...
Mike
2
@EricLippert Entre "n'utilisez pas le multithreading" et "si vous devez, utilisez des structures de données immuables", j'insérerais le niveau intermédiaire et très courant de "faire en sorte qu'un thread enfant utilise uniquement des objets d'entrée appartenant exclusivement et le thread parent consomme les résultats seulement quand l'enfant a fini ». Comme dans var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
John
1
@John: C'est une bonne idée. J'essaie de traiter les threads comme des processus bon marché: ils sont là pour faire un travail et produire un résultat, pas pour courir comme un deuxième thread de contrôle à l'intérieur des structures de données du programme principal. Mais si la quantité de travail que le thread fait est si grande qu'il est raisonnable de la traiter comme un processus, alors je dis qu'il suffit d'en faire un processus!
Eric Lippert
9

Soit votre collègue se trompe, soit il sait quelque chose que la spécification du langage C # ne sait pas.

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 reference."

Ainsi, vous pouvez écrire dans la référence volatile sans risquer d'obtenir une valeur corrompue.

Vous devez bien sûr faire attention à la manière dont vous décidez quel thread doit récupérer les nouvelles données, afin de minimiser le risque que plus d'un thread à la fois le fasse.

Guffa
la source
3
@guffa: oui j'ai lu ça aussi. cela laisse la question d'origine "l'affectation de référence est atomique alors pourquoi Interlocked.Exchange (ref Object, Object) est-il nécessaire?" sans réponse
char m
@zebrabox: que voulez-vous dire? quand ils ne le sont pas? Qu'est-ce que tu ferais?
char m
@matti: C'est nécessaire lorsque vous devez lire et écrire une valeur sous forme d'opération atomique.
Guffa
À quelle fréquence devez-vous vous soucier du fait que la mémoire ne soit pas vraiment alignée correctement dans .NET? Des trucs lourds d'interopérabilité?
Skurmedel
1
@zebrabox: La spécification ne liste pas cette mise en garde, elle donne une déclaration très claire. Avez-vous une référence pour une situation non alignée sur la mémoire où une référence en lecture ou en écriture ne parvient pas à être atomique? Il semble que cela violerait le langage très clair de la spécification.
TJ Crowder
6

Interlocked.Exchange <T>

Définit une variable du type spécifié T sur une valeur spécifiée et renvoie la valeur d'origine, sous la forme d'une opération atomique.

Il change et renvoie la valeur d'origine, c'est inutile car vous ne voulez que le changer et, comme l'a dit Guffa, c'est déjà atomique.

À moins qu'un profileur ne prouve qu'il s'agit d'un goulot d'étranglement dans votre application, vous devriez envisager de déverrouiller les verrous, c'est plus facile à comprendre et à prouver que votre code est correct.

Guillaume
la source
3

Iterlocked.Exchange() n'est pas seulement atomique, il s'occupe également de la visibilité de la mémoire:

Les fonctions de synchronisation suivantes utilisent les barrières appropriées pour garantir l'ordre de la mémoire:

Fonctions qui entrent ou sortent des sections critiques

Fonctions qui signalent des objets de synchronisation

Fonctions d'attente

Fonctions verrouillées

Problèmes de synchronisation et de multiprocesseur

Cela signifie qu'en plus de l'atomicité, il garantit que:

  • Pour le fil qui l'appelle:
    • Aucune réorganisation des instructions n'est effectuée (par le compilateur, le run-time ou le matériel).
  • Pour tous les fils:
    • Aucune lecture en mémoire qui se produit avant cette instruction ne verra le changement effectué par cette instruction.
    • Toutes les lectures après cette instruction verront le changement effectué par cette instruction.
    • Toutes les écritures en mémoire après cette instruction se produiront une fois que ce changement d'instruction aura atteint la mémoire principale (en vidant cette instruction, changez-la dans la mémoire principale une fois terminée et ne laissez pas le matériel le vider en lui appartenant).
selalerer
la source