Pourquoi ce programme ne va-t-il pas dans la boucle infinie en l’absence de la volatilité d’une variable d’état booléenne?

Je voulais comprendre quand exactement je dois déclarer une variable volatile. Pour cela, j’ai écrit un petit programme et m’attendais à ce qu’il passe en boucle infinie à cause de la volatilité manquante d’une variable de condition. Il n’est pas entré dans la boucle infinie et a bien fonctionné sans mot clé volatile.

Deux questions:

  1. Que dois-je changer dans la liste de code ci-dessous – de sorte que cela nécessite absolument l’utilisation de volatile?

  2. Le compilateur C # est-il assez intelligent pour traiter une variable comme volatile – s’il voit qu’une variable est en cours d’access depuis un autre thread?

Ce qui précède a suscité plus de questions pour moi 🙂

une. La volatilité est-elle juste un indice?

b. Quand devrais-je déclarer une variable volatile dans le contexte du multithreading?

c. Toutes les variables membres doivent-elles être déclarées volatiles pour une classe thread-safe? Est-ce exagéré?

Liste de code (la priorité est la volatilité et non la sécurité des threads):

class Program { static void Main(ssortingng[] args) { VolatileDemo demo = new VolatileDemo(); demo.Start(); Console.WriteLine("Completed"); Console.Read(); } } public class VolatileDemo { public VolatileDemo() { } public void Start() { var thread = new Thread(() => { Thread.Sleep(5000); stop = true; }); thread.Start(); while (stop == false) Console.WriteLine("Waiting For Stop Event"); } private bool stop = false; } 

Merci.

Essayez de le réécrire comme ceci:

  public void Start() { var thread = new Thread(() => { Thread.Sleep(5000); stop = true; }); thread.Start(); bool unused = false; while (stop == false) unused = !unused; // fake work to prevent optimization } 

Et assurez-vous que vous utilisez le mode Release et non le mode Debug. En mode Release, des optimisations sont appliquées, ce qui entraîne l’échec du code en l’absence de données volatile .

Edit : Un peu de volatile :

Nous soaps tous qu’il existe deux entités distinctes impliquées dans un cycle de vie de programme et pouvant appliquer des optimisations sous la forme de mise en cache de variables et / ou de réorganisation d’instructions: le compilateur et la CPU.

Cela signifie qu’il peut même y avoir une grande différence entre la manière dont vous avez écrit votre code et la façon dont il est exécuté, dans la mesure où les instructions peuvent être réorganisées les unes par rapport aux autres, ou que les lectures peuvent être mises en cache dans ce que le compilateur considère comme une “amélioration de la vitesse.” “.

La plupart du temps, c’est une bonne chose, mais parfois (surtout dans le contexte du multithreading), cela peut causer des problèmes, comme dans cet exemple. Pour permettre au programmeur d’empêcher manuellement de telles optimisations, des barrières de mémoire ont été introduites, des instructions spéciales ayant pour rôle d’empêcher à la fois la réorganisation d’instructions (lecture, écriture ou les deux à la fois) par rapport à la barrière elle-même et de forcer l’invalidation de valeurs. dans les caches de processeur, de sorte qu’ils doivent être relus à chaque fois (c’est ce que nous voulons dans le scénario ci-dessus).

Bien que vous puissiez spécifier une clôture complète affectant toutes les variables via Thread.MemoryBarrier() , il est presque toujours excessif si vous n’avez besoin que d’une seule variable. Ainsi, pour qu’une seule variable soit toujours à jour sur tous les threads, vous pouvez utiliser volatile pour introduire des clôtures en lecture / écriture pour cette variable uniquement.

Tout d’abord, Joe Duffy dit que “l’instabilité est un mal” , c’est suffisant pour moi.

Si vous voulez vraiment penser à volatile, vous devez penser en termes de barrière de mémoire et d’optimisation – par le compilateur, la gigue et le processeur.

Sur x86, les écritures sont des clôtures de publication, ce qui signifie que votre thread d’arrière-plan effacera la valeur true en mémoire.

Donc, ce que vous recherchez est une mise en cache de la valeur false dans votre prédicat de boucle. Le compliant ou le jitter peut optimiser le prédicat et ne l’évaluer qu’une seule fois, mais je suppose qu’il ne le fait pas pour la lecture d’un champ de classe. La CPU ne mettra pas en cache la valeur false car vous appelez Console.WriteLine qui inclut une clôture.

Ce code nécessite une version volatile et ne se terminera jamais sans un Volatile.Read :

 static void Run() { bool stop = false; Task.Factory.StartNew( () => { Thread.Sleep( 1000 ); stop = true; } ); while ( !stop ) ; } 

Je ne suis pas un expert en concurrence de C #, mais autant que je sache, vos attentes sont incorrectes. La modification d’une variable non volatile à partir d’un thread différent ne signifie pas que le changement ne sera jamais visible par les autres threads. Seulement qu’il n’y a aucune garantie quand (et si) cela se produit . Dans votre cas, cela s’est produit (combien de fois avez-vous exécuté le programme d’ailleurs?), Probablement en raison du fil final qui efface ses modifications, conformément au commentaire de @ Russell. Mais dans une configuration réelle – impliquant un stream de programme plus complexe, plus de variables, plus de threads – la mise à jour peut avoir lieu plus tard que 5 secondes, ou – peut-être une fois sur mille – peut ne pas se produire du tout.

Ainsi, exécuter votre programme une fois, voire un million de fois, sans observer aucun problème ne fournit que des preuves statistiques et non absolues. “L’absence de preuve n’est pas une preuve d’absence” .

Le mot clé volatile est un message destiné à un compilateur pour ne pas effectuer d’optimisations à thread unique sur cette variable. Cela signifie que cette variable peut être modifiée par plusieurs threads. Cela rend la valeur de variable la plus «fraîche» lors de la lecture.

Le morceau de code que vous avez collé ici est un bon exemple d’utilisation d’un mot clé volatile. Ce n’est pas une surprise si ce code fonctionne sans mot clé ‘volatile’. Cependant, il peut se comporter de manière plus imprévisible lorsque plusieurs threads sont en cours d’exécution et que vous effectuez des actions plus sophistiquées sur la valeur de l’indicateur.

Vous déclarez volatile uniquement sur les variables pouvant être modifiées par plusieurs threads. Je ne sais pas exactement ce qu’il en est en C #, mais je suppose que vous ne pouvez pas utiliser volatile pour les variables modifiées par des actions en lecture-écriture (telles que l’incrémentation). Volatile n’utilise pas de verrou lors de la modification de la valeur. Il est donc correct de définir l’indicateur sur volatile (comme ci-dessus), l’incrémentation de la variable n’est pas correcte – vous devez alors utiliser le mécanisme de synchronisation / locking.

Lorsque le thread en arrière-plan atsortingbue la valeur true à la variable membre, il existe une clôture de publication, la valeur est écrite en mémoire et le cache de l’autre processeur est mis à jour ou vidé de cette adresse.

L’appel de fonction à Console.WriteLine est une barrière de mémoire complète et sa sémantique selon laquelle il est possible de faire quoi que ce soit (à l’exception des optimisations du compilateur) nécessiterait que l’ stop ne soit pas mis en cache.

Cependant, si vous supprimez l’appel de Console.WriteLine , je constate que la fonction est toujours en cours d’arrêt.

Je crois que le compilateur en l’absence d’optimisations le compilateur ne cache rien calculé à partir de la mémoire globale. Le mot clé volatile est alors une instruction pour ne même pas penser à mettre en cache une expression impliquant la variable dans le compilateur / JIT.

Ce code s’arrête toujours (du moins pour moi, j’utilise Mono):

 public void Start() { stop = false; var thread = new Thread(() => { while(true) { Thread.Sleep(50); stop = !stop; } }); thread.Start(); while ( !(stop ^ stop) ); } 

Cela montre qu’il ne s’agit pas d’une instruction while empêchant la mise en cache, car elle indique que la variable n’est pas mise en cache, même dans la même déclaration.

Cette optimisation semble sensible au modèle de mémoire, qui dépend de la plate-forme, ce qui signifie que cela devrait être fait dans le compilateur JIT; qui n’aurait pas le temps (ou l’intelligence) de / voir / l’utilisation de la variable dans l’autre thread et empêcher la mise en cache pour cette raison.

Peut-être Microsoft ne croit-il pas que les programmeurs sont capables de savoir quand utiliser des composants volatiles et a décidé de les en décharger, puis Mono a emboîté le pas.