Supposons que j'ai un champ int non volatile et un thread qui le Interlocked.Increment
. Un autre thread peut-il lire ceci directement en toute sécurité, ou la lecture doit-elle également être verrouillée?
Je pensais auparavant que je devais utiliser une lecture verrouillée pour garantir que je vois la valeur actuelle, car, après tout, le champ n'est pas volatile. J'utilise Interlocked.CompareExchange(int, 0, 0)
pour y parvenir.
Cependant, je suis tombé sur cette réponse qui suggère que les lectures simples verront toujours la version actuelle d'une valeur Interlocked.Increment
Ed, et puisque la lecture int est déjà atomique, il n'y a pas besoin faire quelque chose de spécial. J'ai également trouvé ne demande dans laquelle Microsoft rejette une demande pour Interlocked.Read (ref int) , suggérant en outre que cela est complètement redondant.
Alors, puis-je vraiment lire en toute sécurité la valeur la plus récente d'un tel champ int
sans Interlocked
?
Si vous voulez garantir que l'autre thread lira la dernière valeur, vous devez utiliser Thread.VolatileRead()
. (*)
L'opération de lecture elle-même est atomique, ce qui ne causera aucun problème, mais sans lecture volatile, vous pouvez obtenir l'ancienne valeur du cache ou le compilateur peut optimiser votre code et éliminer complètement l'opération de lecture. Du point de vue du compilateur, il suffit que le code fonctionne dans un environnement à thread unique. Les opérations volatiles et les barrières mémoire sont utilisées pour limiter la capacité du compilateur à optimiser et réorganiser le code.
Plusieurs participants peuvent modifier le code: compilateur, compilateur JIT et CPU. Peu importe lequel d'entre eux montre que votre code est cassé. La seule chose importante est le .Modèle de mémoire NET car il spécifie les règles qui doivent être respectées par tous les participants.
(*) Thread.VolatileRead()
n'obtient pas vraiment la dernière valeur. Il lira la valeur et ajoutera une barrière de mémoire après la lecture. La première lecture volatile peut obtenir une valeur mise en cache mais la seconde obtiendrait une valeur mise à jour car la barrière de mémoire de la première lecture volatile a forcé une mise à jour du cache si cela était nécessaire. En pratique, ce détail a peu d'importance lors de l'écriture du code.
Un peu un problème de méta, mais un bon aspect sur l'utilisation de Interlocked.CompareExchange(ref value, 0, 0)
(en ignorant l'inconvénient évident qu'il est plus difficile à comprendre lorsqu'il est utilisé pour la lecture) est qu'il fonctionne indépendamment de int
ou long
. Il est vrai que les lectures int
sont toujours atomiques, mais les lectures long
ne le sont pas ou peuvent ne pas l'être, selon l'architecture. Malheureusement, Interlocked.Read(ref value)
ne fonctionne que si value
est de type long
.
Considérez le cas où vous commencez par un champ int
, ce qui rend impossible l'utilisation de Interlocked.Read()
, vous lirez donc la valeur directement à la place car c'est atomique de toute façon. Cependant, plus tard dans le développement, vous ou quelqu'un d'autre décide qu'un long
est requis - le compilateur ne vous avertira pas, mais maintenant vous pouvez avoir un bug subtil: l'accès en lecture n'est plus garanti d'être atomique. J'ai trouvé en utilisant Interlocked.CompareExchange()
la meilleure alternative ici; Il peut être plus lent en fonction des instructions du processeur sous-jacent, mais il est plus sûr à long terme. Je ne connais pas assez les internes de Thread.VolatileRead()
cependant; Il pourrait être "meilleur" concernant ce cas d'utilisation car il fournit encore plus de signatures.
Je n'essaierais pas de lire la valeur directement (c'est-à-dire sans aucun des mécanismes ci-dessus) dans une boucle ou tout appel de méthode serré, car même si les écritures sont volatiles et/ou barrées en mémoire, rien ne dit au compilateur que le la valeur du champ peut en fait changer entre deux lit. Ainsi, le champ doit être soit volatile
, soit l'une des constructions données doit être utilisée.
Mes deux centimes.
Vous avez raison, vous n'avez pas besoin d'une instruction spéciale pour lire atomiquement un entier 32 bits, cependant, cela signifie que vous obtiendrez la valeur "entière" (c'est-à-dire que vous n'obtiendrez pas la partie d'une écriture et d'une partie d'une autre ). Vous n'avez aucune garantie que la valeur n'aura pas changé une fois que vous l'avez lue.
C'est à ce stade que vous devez décider si vous devez utiliser une autre méthode de synchronisation pour contrôler l'accès, par exemple si vous utilisez cette valeur pour lire un membre d'un tableau, etc.
En un mot, atomicité garantit qu'une opération se déroule de manière complète et indivisible. Étant donné une opération A
qui contenait N
étapes, si vous êtes arrivé à l'opération juste après A
, vous pouvez être assuré que toutes les N
étapes se sont déroulées isolément des opérations simultanées.
Si vous aviez deux threads qui ont exécuté l'opération atomique A
vous avez la garantie que vous ne verrez que le résultat complet de l'un des deux threads. Si vous souhaitez coordonner les threads, des opérations atomiques peuvent être utilisées pour créer la synchronisation requise. Mais les opérations atomiques ne fournissent pas en elles-mêmes une synchronisation de niveau supérieur. La famille de méthodes Interlocked
est mise à disposition pour fournir certaines opérations atomiques fondamentales.
Synchronisation est un type plus large de contrôle d'accès simultané, souvent construit autour d'opérations atomiques . La plupart des processeurs incluent des barrières de mémoire qui vous permettent de vous assurer que toutes les lignes de cache sont vidées et que vous avez une vue cohérente de la mémoire. Les lectures volatiles sont un moyen d'assurer un accès cohérent à un emplacement mémoire donné.
Bien qu'elle ne s'applique pas immédiatement à votre problème, la lecture d'ACID (atomicité, cohérence, isolement et durabilité) en ce qui concerne les bases de données peut vous aider avec la terminologie.