En programmation informatique , une valeur est dite volatile si elle peut être lue ou modifiée de manière asynchrone par autre chose que le thread d'exécution actuel . La valeur d'une volatilevariable peut changer spontanément pour des raisons telles que : le partage de valeurs avec d'autres threads ; le partage de valeurs avec des gestionnaires de signaux asynchrones ; l'accès à des périphériques matériels via des E/S mappées en mémoire (où vous pouvez envoyer et recevoir des messages à partir de périphériques en lisant et en écrivant dans la mémoire). La prise en charge de ces cas d'utilisation varie considérablement entre les différents langages de programmation qui ont le mot-clé. La volatilité peut avoir des implications sur les conventions d'appelvolatile de fonction et sur la manière dont les variables sont stockées, consultées et mises en cache.
En C et C++
En C et C++, volatileest un qualificateur de type , comme const, et fait partie d'un type (par exemple, le type d'une variable ou d'un champ).
Le comportement du volatilemot-clé en C et C++ est parfois donné en termes de suppression des optimisations d'un compilateur optimisant : 1- ne pas supprimer volatileles lectures et écritures existantes, 2- ne pas ajouter de nouvelles volatilelectures et écritures, et 3- ne pas réorganiser volatileles lectures et écritures. Cependant, cette définition n'est qu'une approximation au bénéfice des nouveaux apprenants, et cette définition approximative ne doit pas être utilisée pour écrire du code de production réel.
En C, et par conséquent en C++, le volatilemot-clé était destiné à :
- Autoriser l'accès aux périphériques d'E/S mappés en mémoire .
- Permet de préserver les valeurs sur un
longjmp. - Autoriser le partage de valeurs entre les gestionnaires de signaux et le reste du programme dans
volatilesig_atomic_tles objets.
Les normes C et C++ permettent d'écrire du code portable qui partage des valeurs entre des longjmpobjets volatile, et les normes permettent d'écrire du code portable qui partage des valeurs entre des gestionnaires de signaux et le reste du code dans volatile
sig_atomic_tdes objets. Toute autre utilisation du volatilemot-clé en C et C++ est intrinsèquement non portable ou incorrecte. En particulier, l'écriture de code avec le volatilemot-clé pour les périphériques d'E/S mappés en mémoire est intrinsèquement non portable et nécessite toujours une connaissance approfondie de l'implémentation et de la plateforme C/C++ cible spécifiques.
Multithreading
Il s'agit d'une idée fausse courante selon laquelle le volatilemot-clé est utile dans le code multithreadvolatile portable en C et C++. Le mot-clé en C et C++ n'a jamais fonctionné comme un outil utile et portable pour aucun scénario multithread. Contrairement aux langages de programmation Java et C# , les opérations sur volatileles variables en C et C++ ne sont pas atomiques et les opérations sur volatileles variables n'ont pas de garanties d'ordre de mémoire suffisantes (c'est-à-dire des barrières de mémoire ). La plupart des compilateurs, éditeurs de liens et runtimes C et C++ ne fournissent tout simplement pas les garanties d'ordre de mémoire nécessaires pour rendre le volatilemot-clé utile pour tout scénario multithread. Avant les normes C11 et C++11, les programmeurs étaient obligés de s'appuyer sur les garanties des implémentations et des plates-formes individuelles (par exemple POSIX et WIN32) pour écrire du code multithread . Avec les normes C11 et C++11 modernes, les programmeurs peuvent écrire du code multithread portable en utilisant de nouvelles constructions portables telles que les std::atomic<T>modèles.
Exemple d'E/S mappées en mémoire en C
Dans cet exemple, le code définit la valeur stockée dans foosur 0. Il commence ensuite à interroger cette valeur à plusieurs reprises jusqu'à ce qu'elle devienne 255:
statique int foo ; vide barre ( vide ) { foo = 0 ; tandis que ( foo != 255 ) ; }
Un compilateur optimisant remarquera qu'aucun autre code ne peut modifier la valeur stockée dans foo, et supposera qu'elle restera égale à 0à tout moment. Le compilateur remplacera donc le corps de la fonction par une boucle infinie similaire à celle-ci :
void bar_optimized ( void ) { foo = 0 ; tant que ( vrai ) ; }
Cependant, le programmeur peut faire fooréférence à un autre élément du système informatique tel qu'un registre matériel d'un périphérique connecté au processeur qui peut modifier la valeur de foopendant que ce code est en cours d'exécution. (Cet exemple n'inclut pas les détails sur la façon de faire fooréférence à un registre matériel d'un périphérique connecté au processeur.) Sans le volatilemot-clé, un compilateur optimisant convertira probablement le code du premier échantillon avec la lecture dans la boucle au deuxième échantillon sans la lecture dans la boucle dans le cadre de l' optimisation commune du code-mouvement invariant en boucle , et donc le code ne remarquera probablement jamais le changement qu'il attend.
Pour empêcher le compilateur de faire cette optimisation, le volatilemot clé peut être utilisé :
statique volatile int foo ; vide barre ( vide ) { foo = 0 ; tandis que ( foo != 255 ) ; }
Le volatilemot clé empêche le compilateur de déplacer la lecture hors de la boucle, et ainsi le code remarquera le changement attendu dans la variable foo.
Comparaison d'optimisation en C
Les programmes C suivants et les extraits de langage assembleur qui les accompagnent montrent comment le volatilemot-clé affecte la sortie du compilateur. Dans ce cas, le compilateur était GCC .
En observant le code assembleur, il est clairement visible que le code généré avec volatiledes objets est plus verbeux, ce qui le rend plus long afin que la nature des volatileobjets puisse être respectée. Le volatilemot-clé empêche le compilateur d'effectuer une optimisation sur le code impliquant des objets volatiles, garantissant ainsi que chaque affectation et lecture de variable volatile a un accès mémoire correspondant. Sans le volatilemot-clé, le compilateur sait qu'une variable n'a pas besoin d'être relue depuis la mémoire à chaque utilisation, car il ne devrait y avoir aucune écriture dans son emplacement mémoire à partir d'un autre thread ou processus.
Défauts de normes
Bien que prévu à la fois par C et C++, la norme C actuelle ne parvient pas à exprimer que la volatilesémantique fait référence à la lvalue, et non à l'objet référencé. Le rapport de défaut respectif DR 476 (à C11) est toujours en cours d'examen avec C17 .
Défauts du compilateur
Contrairement à d'autres fonctionnalités du langage C et C++, le volatilemot-clé n'est pas bien pris en charge par la plupart des implémentations C/C++ - même pour des utilisations portables selon les normes C et C++. La plupart des implémentations C/C++ présentent des bugs concernant le comportement du volatilemot-clé. Les programmeurs doivent être très prudents lorsqu'ils utilisent le volatilemot-clé en C et C++.
En Java
Dans toutes les versions modernes du langage de programmation Java , le volatilemot clé donne les garanties suivantes :
volatileLes lectures et les écritures sont atomiques . En particulier, les lectures et les écritures dans les champslongetdoublene seront pas interrompues. (La garantie atomiquevolatiles'applique uniquement à la valeur primitive ou à lavolatilevaleur de référence, et non à une valeur d'objet.)- Il existe un ordre global unique pour toutes
volatileles lectures et écritures. En d'autres termes, unevolatilelecture lira la valeur actuelle (et non une valeur passée ou future), et toutesvolatileles lectures conviendront d'un ordre global unique d'volatileécritures. volatileLes lectures et les écritures ont une sémantique de barrière de mémoire « acquérir » et « libérer » (connue dans la norme Java sous le nom de happens-before ). En d'autres termes,volatilefournit des garanties sur l'ordre relatif des lectures et des écrituresvolatilenon effectuéesvolatile. En d'autres termes,volatilefournit fondamentalement les mêmes garanties de visibilité de la mémoire qu'un bloc synchronisé Java (mais sans les garanties d'exclusion mutuelle d'un bloc synchronisé ).
Ensemble, ces garanties constituent volatileune construction multithread utile en Java . En particulier, l' algorithme de verrouillage à double vérificationvolatile typique fonctionne correctement en Java .
Les premières versions de Java
Avant la version 5 de Java, la norme Java ne garantissait pas l'ordre relatif des lectures et des écritures volatilenon autorisées volatile. En d'autres termes, volatileelle ne disposait pas de sémantique de barrière mémoire « acquérir » et « libérer » . Cela limitait grandement son utilisation en tant que construction multithread . En particulier, l' algorithme de verrouillage à double vérificationvolatile typique avec ne fonctionnait pas correctement.
En C#
En C# , volatilegarantit que le code accédant au champ n'est pas soumis à des optimisations non sécurisées pour les threads qui peuvent être effectuées par le compilateur, le CLR ou par le matériel. Lorsqu'un champ est marqué volatile, le compilateur est chargé de générer une « barrière mémoire » ou « clôture » autour de lui, ce qui empêche la réorganisation ou la mise en cache des instructions liées au champ. Lors de la lecture d'un volatilechamp, le compilateur génère une clôture d'acquisition , qui empêche les autres lectures et écritures dans le champ d'être déplacées avant la clôture. Lors de l'écriture dans un volatilechamp, le compilateur génère une clôture de libération ; cette clôture empêche les autres lectures et écritures dans le champ d'être déplacées après la clôture.
Seuls les types suivants peuvent être marquésvolatile : tous les types de référence, Single, Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Char, et tous les types énumérés avec un type sous-jacent de Byte, SByte, Int16, UInt16, Int32, ou UInt32. les structures de valeur , ainsi que les types primitifs Double, Int64, UInt64et Decimal.)
L'utilisation du volatilemot-clé ne prend pas en charge les champs transmis par référence ou les variables locales capturées ; dans ces cas, Thread.VolatileReadet Thread.VolatileWritedoit être utilisé à la place.
En effet, ces méthodes désactivent certaines optimisations habituellement effectuées par le compilateur C#, le compilateur JIT ou le processeur lui-même. Les garanties fournies par Thread.VolatileReadet Thread.VolatileWritesont un sur-ensemble des garanties fournies par le volatilemot-clé : au lieu de générer une « demi-clôture » (c'est-à-dire qu'une clôture d'acquisition empêche uniquement la réorganisation et la mise en cache des instructions qui la précèdent), VolatileReadet VolatileWritegénèrent une « clôture complète » qui empêche la réorganisation et la mise en cache des instructions de ce champ dans les deux sens. Ces méthodes fonctionnent comme suit :
- La
Thread.VolatileWriteméthode force l'écriture de la valeur du champ au moment de l'appel. De plus, tous les chargements et stockages antérieurs dans l'ordre du programme doivent avoir lieu avant l'appelVolatileWriteet tous les chargements et stockages ultérieurs dans l'ordre du programme doivent avoir lieu après l'appel. - La
Thread.VolatileReadméthode force la lecture de la valeur du champ au moment de l'appel. De plus, tous les chargements et stockages antérieurs dans l'ordre du programme doivent avoir lieu avant l'appelVolatileReadet tous les chargements et stockages ultérieurs dans l'ordre du programme doivent avoir lieu après l'appel.
Les méthodes Thread.VolatileReadet Thread.VolatileWritegénèrent une clôture complète en appelant la Thread.MemoryBarrierméthode , qui construit une barrière mémoire qui fonctionne dans les deux sens. En plus des motivations pour l'utilisation d'une clôture complète données ci-dessus, un problème potentiel avec le volatilemot-clé qui est résolu en utilisant une clôture complète générée par Thread.MemoryBarrierest le suivant : en raison de la nature asymétrique des demi-clôtures, un volatilechamp avec une instruction d'écriture suivie d'une instruction de lecture peut toujours avoir l'ordre d'exécution inversé par le compilateur. Étant donné que les clôtures complètes sont symétriques, ce n'est pas un problème lors de l'utilisation de Thread.MemoryBarrier.
En Fortran
VOLATILEfait partie de la norme Fortran 2003 , bien que les versions antérieures la prenaient en charge en tant qu'extension. Rendre toutes les variables volatiled'une fonction utile est également utile pour trouver des bogues liés aux alias .
entier , volatile :: i ! Lorsqu'il n'est pas défini comme volatile, les deux lignes de code suivantes sont identiques write ( * , * ) i ** 2 ! Charge la variable i une fois depuis la mémoire et multiplie cette valeur par elle-même write ( * , * ) i * i ! Charge la variable i deux fois depuis la mémoire et multiplie ces valeurs
En « creusant » toujours jusqu'à la mémoire d'un VOLATILE, le compilateur Fortran est empêché de réorganiser les lectures ou les écritures dans les volatiles. Cela rend visibles aux autres threads les actions effectuées dans ce thread, et vice versa.
L'utilisation de VOLATILE réduit et peut même empêcher l'optimisation.