(Ceci est une répétition de: Comment lire correctement un champ int Interlocked.Increment'ed? mais, après avoir lu les réponses et les commentaires, je ne suis toujours pas sûr de la bonne réponse.)
Il y a du code que je ne possède pas et que je ne peux pas changer pour utiliser des verrous qui incrémentent un compteur int (numberOfUpdates) dans plusieurs threads différents. Tous les appels utilisent:
Interlocked.Increment(ref numberOfUpdates);
Je veux lire numberOfUpdates dans mon code. Maintenant que c'est un int, je sais qu'il ne peut pas se déchirer. Mais quelle est la meilleure façon de m'assurer d'obtenir la dernière valeur possible? Il semble que mes options soient:
int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);
Ou
int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);
Les deux fonctionneront-ils (dans le sens de fournir la dernière valeur possible indépendamment des optimisations, des réorganisations, de la mise en cache, etc.)? Est-ce que l'un est préféré à l'autre? Y a-t-il une troisième option qui est meilleure?
Je suis fermement convaincu que si vous utilisez un verrouillage pour incrémenter les données partagées, vous devez utiliser le verrouillage partout où vous accédez à ces données partagées. De même, si vous utilisez insérez votre primitive de synchronisation préférée ici pour incrémenter les données partagées, vous devez utiliser insérez votre favori la primitive de synchronisation ici partout où vous accédez à ces données partagées.
int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);
Vous donnera exactement ce que vous cherchez. Comme d'autres l'ont dit, les opérations interdépendantes sont atomiques. Ainsi, Interlocked.CompareExchange renverra toujours la valeur la plus récente. Je l'utilise tout le temps pour accéder à des données partagées simples comme des compteurs.
Je ne suis pas aussi familier avec Thread.VolatileRead, mais je pense qu'il renverra également la valeur la plus récente. Je m'en tiendrai aux méthodes interdépendantes, ne serait-ce que pour être cohérent.
Information additionnelle:
Je recommanderais de jeter un coup d'œil à la réponse de Jon Skeet pour savoir pourquoi vous voudrez peut-être vous éloigner de Thread.VolatileRead (): Thread.VolatileRead Implementation
Eric Lippert discute de la volatilité et des garanties apportées par le modèle de mémoire C # dans son blog à http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability -sont-different-part-three.aspx . Directement de la bouche du cheval: "Je n'essaie pas d'écrire de code low-lock sauf pour les utilisations les plus triviales des opérations Interlocked. Je laisse l'utilisation de" volatile "à de vrais experts."
Et je suis d'accord avec le point de Hans selon lequel la valeur sera toujours périmée au moins de quelques ns, mais si vous avez un cas d'utilisation où cela est inacceptable, ce n'est probablement pas bien adapté pour un langage de récupération des ordures comme C # ou un non réel - temps OS. Joe Duffy a un bon article sur l'actualité des méthodes imbriquées ici: http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/
Thread.VolatileRead(numberOfUpdates)
est ce que vous voulez. numberOfUpdates
est un Int32
, vous avez donc déjà l'atomicité par défaut, et Thread.VolatileRead
assurera la gestion de la volatilité.
Si numberOfUpdates
est défini comme volatile int numberOfUpdates;
vous n'avez pas à le faire, car toutes les lectures seront déjà des lectures volatiles.
Il semble y avoir confusion quant à savoir si Interlocked.CompareExchange
est plus approprié. Considérez les deux extraits suivants de la documentation.
Du Thread.VolatileRead
Documentation:
Lit la valeur d'un champ. La valeur est la dernière écrite par un processeur d'un ordinateur, quel que soit le nombre de processeurs ou l'état du cache de processeur.
Du Interlocked.CompareExchange
Documentation:
Compare l'égalité de deux entiers signés 32 bits et, s'ils sont égaux, remplace l'une des valeurs.
En termes de comportement déclaré de ces méthodes, Thread.VolatileRead
est clairement plus approprié. Vous ne voulez pas comparer numberOfUpdates
à une autre valeur et vous ne voulez pas remplacer sa valeur. Vous voulez lire sa valeur.
Lasse fait un bon point dans son commentaire: il vaut mieux utiliser un simple verrouillage. Lorsque l'autre code veut mettre à jour numberOfUpdates
, il fait quelque chose comme ceci.
lock (state)
{
state.numberOfUpdates++;
}
Lorsque vous voulez le lire, vous faites quelque chose comme ce qui suit.
int value;
lock (state)
{
value = state.numberOfUpdates;
}
Cela garantira vos exigences d'atomicité et de volatilité sans plonger dans des primitives de multithreading plus obscures et de relativement bas niveau.
Les deux fonctionneront-ils (dans le sens de fournir la dernière valeur possible indépendamment des optimisations, des réorganisations, de la mise en cache, etc.)?
Non, la valeur que vous obtenez est toujours périmée. L'impuissance de la valeur est totalement imprévisible. La grande majorité du temps, il sera périmé de quelques nanosecondes, donner ou prendre, selon la rapidité avec laquelle vous agissez sur la valeur. Mais il n'y a pas de limite supérieure raisonnable:
Tout ce que vous faites avec la valeur doit en tenir compte. Inutile de dire que c'est très, très difficile. Les exemples pratiques sont difficiles à trouver. Le .NET Framework est un très gros morceau de code marqué au combat. Vous pouvez voir la référence croisée à l'utilisation de VolatileRead à partir de la source de référence . Nombre de hits: 0.
Eh bien, toute valeur que vous lirez sera toujours un peu obsolète, comme l'a dit Hans Passant. Vous ne pouvez contrôler qu'une garantie que les autres valeurs partagées sont cohérentes avec celle que vous venez de lire en utilisant des clôtures de mémoire au milieu du code lisant plusieurs valeurs partagées sans serrures (c.-à-d.: sont au même degré de "vétusté")
Les clôtures ont également pour effet de déjouer certaines optimisations du compilateur et de les réorganiser, empêchant ainsi un comportement inattendu en mode de publication sur différentes plates-formes.
Thread.VolatileRead provoquera l'émission d'une clôture de mémoire complète afin qu'aucune lecture ou écriture ne puisse être réorganisée autour de votre lecture de l'int (dans la méthode qui la lit). Évidemment, si vous ne lisez qu'une seule valeur partagée (et que vous ne lisez pas autre chose partagée et que l'ordre et la cohérence des deux sont importants), cela peut ne pas sembler nécessaire ...
Mais je pense que vous en aurez besoin de toute façon pour vaincre certaines optimisations par le compilateur ou le processeur afin que vous n'obteniez pas la lecture plus "périmée" que nécessaire.
Un Interlocked.CompareExchange factice fera la même chose que Thread.VolatileRead (clôture complète et comportement défaisant de l'optimisation).
Il existe un modèle suivi dans le cadre utilisé par CancellationTokenSource http://referencesource.Microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs#64
//m_state uses the pattern "volatile int32 reads, with cmpxch writes" which is safe for updates and cannot suffer torn reads.
private volatile int m_state;
public bool IsCancellationRequested
{
get { return m_state >= NOTIFYING; }
}
// ....
if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED) {
}
// ....
Le mot-clé volatile a pour effet d'émettre une "demi" clôture. (c.-à-d.: il empêche les lectures/écritures d'être déplacées avant la lecture, et empêche les lectures/écritures d'être déplacées après l'écriture).