¿Para qué se usa la palabra clave "volátil"?

130

Leí algunos artículos sobre la volatilepalabra clave pero no pude averiguar su uso correcto. ¿Podría decirme para qué se debe usar en C # y en Java?

Mircea
fuente
1
Uno de los problemas con volátil es que significa más de una cosa. Ser información para el compilador para no hacer optimizaciones funky es un legado de C. Es también significa que las barreras de memoria se deben utilizar en el acceso. Pero en la mayoría de los casos solo cuesta rendimiento y / o confunde a las personas. : P
AnorZaken

Respuestas:

93

Tanto para C # como para Java, "volátil" le dice al compilador que el valor de una variable nunca debe almacenarse en caché ya que su valor puede cambiar fuera del alcance del programa en sí. El compilador evitará las optimizaciones que puedan causar problemas si la variable cambia "fuera de su control".

Will A
fuente
@Tom - debidamente notado, señor - y modificado.
Will A
11
Todavía es mucho más sutil que eso.
Tom Hawtin - tackline
1
Incorrecto. No evita el almacenamiento en caché. Mira mi respuesta.
doug65536
168

Considere este ejemplo:

int i = 5;
System.out.println(i);

El compilador puede optimizar esto para imprimir solo 5, así:

System.out.println(5);

Sin embargo, si hay otro hilo que puede cambiar i, este es el comportamiento incorrecto. Si otro hilo cambia ia 6, la versión optimizada seguirá imprimiendo 5.

La volatilepalabra clave evita dicha optimización y almacenamiento en caché, y por lo tanto es útil cuando una variable puede ser cambiada por otro hilo.

Sjoerd
fuente
3
Creo que la optimización aún sería válida con imarcado como volatile. En Java, se trata de relaciones antes de pasar .
Tom Hawtin - tackline
Gracias por publicar, así que de alguna manera volátil tiene conexiones con bloqueo variable?
Mircea
@Mircea: Eso es lo que me dijeron que marcar algo como volátil se trataba: marcar un campo como volátil usaría algún mecanismo interno para permitir que los hilos vean un valor consistente para la variable dada, pero esto no se menciona en la respuesta anterior ... tal vez alguien puede confirmar esto o no? Gracias
npinti
55
@Sjoerd: No estoy seguro de entender este ejemplo. Si ies una variable local, ningún otro hilo puede cambiarla de todos modos. Si es un campo, el compilador no puede optimizar la llamada a menos que lo sea final. No creo que el compilador pueda hacer optimizaciones basadas en suponer que un campo "se ve" finalcuando no se declara explícitamente como tal.
Polygenelubricants
1
C # y Java no son C ++. Esto no es correcto. No evita el almacenamiento en caché y no impide la optimización. Se trata de la semántica de lectura-adquisición y almacenamiento-liberación, que se requieren en arquitecturas de memoria con un orden débil. Se trata de ejecución especulativa.
doug65536
40

Para comprender lo que la volatilidad le hace a una variable, es importante entender qué sucede cuando la variable no es volátil.

  • La variable es no volátil

Cuando dos hilos A y B acceden a una variable no volátil, cada hilo mantendrá una copia local de la variable en su caché local. Cualquier cambio realizado por el hilo A en su caché local no será visible para el hilo B.

  • La variable es volátil

Cuando las variables se declaran volátiles, esencialmente significa que los hilos no deben almacenar en caché dicha variable o, en otras palabras, los hilos no deben confiar en los valores de estas variables a menos que se lean directamente de la memoria principal.

Entonces, ¿cuándo hacer una variable volátil?

Cuando tiene una variable a la que pueden acceder muchos subprocesos y desea que cada subproceso obtenga el último valor actualizado de esa variable, incluso si el otro subproceso / proceso / fuera del programa actualiza el valor.

Saurabh Patil
fuente
2
Incorrecto. No tiene nada que ver con "evitar el almacenamiento en caché". Se trata de reordenar, por el compilador, O el hardware de la CPU a través de la ejecución especulativa.
doug65536
37

Las lecturas de campos volátiles han adquirido semántica . Esto significa que se garantiza que la lectura de memoria de la variable volátil ocurrirá antes de cualquier lectura de memoria siguiente. Impide que el compilador realice el reordenamiento, y si el hardware lo requiere (CPU débilmente ordenada), utilizará una instrucción especial para hacer que el hardware elimine cualquier lectura que ocurra después de la lectura volátil pero que se inició especulativamente antes, o la CPU podría evite que se emitan temprano en primer lugar, evitando que ocurra cualquier carga especulativa entre la emisión de la carga adquirida y su retiro.

Las escrituras de campos volátiles tienen semántica de lanzamiento . Esto significa que se garantiza que cualquier escritura de memoria en la variable volátil se retrasará hasta que todas las escrituras de memoria anteriores sean visibles para otros procesadores.

Considere el siguiente ejemplo:

something.foo = new Thing();

Si fooes una variable miembro en una clase, y otras CPU tienen acceso a la instancia del objeto mencionada something, ¡podrían ver el foocambio de valor antes de que las escrituras de memoria en el Thingconstructor sean visibles globalmente! Esto es lo que significa "memoria débilmente ordenada". Esto podría ocurrir incluso si el compilador tiene todas las tiendas en el constructor antes de la tienda foo. Si fooes volatileentonces, la tienda footendrá semántica de lanzamiento, y el hardware garantiza que todas las escrituras antes de la escritura foosean visibles para otros procesadores antes de permitir fooque ocurra la escritura .

¿Cómo es posible que los escritos foose reordenen tan mal? Si la retención de línea de caché fooestá en el caché, y las tiendas en el constructor perdieron el caché, entonces es posible que la tienda se complete mucho antes de lo que fallan las escrituras en el caché.

La (horrible) arquitectura Itanium de Intel tenía una memoria débilmente ordenada. El procesador utilizado en el XBox 360 original tenía una memoria débilmente ordenada. Muchos procesadores ARM, incluido el muy popular ARMv7-A, tienen poca memoria ordenada.

Los desarrolladores a menudo no ven estas carreras de datos porque cosas como los bloqueos harán una barrera de memoria completa, esencialmente lo mismo que adquirir y liberar semántica al mismo tiempo. No se pueden ejecutar especulativamente cargas dentro del bloqueo antes de que se adquiera el bloqueo, se retrasan hasta que se adquiere el bloqueo. No se pueden retrasar las tiendas a través de una liberación de bloqueo, la instrucción que libera el bloqueo se retrasa hasta que todas las escrituras realizadas dentro del bloqueo sean visibles globalmente.

Un ejemplo más completo es el patrón de "bloqueo de doble verificación". El propósito de este patrón es evitar tener que adquirir siempre un bloqueo para inicializar un objeto de forma diferida.

Enganchado de Wikipedia:

public class MySingleton {
    private static object myLock = new object();
    private static volatile MySingleton mySingleton = null;

    private MySingleton() {
    }

    public static MySingleton GetInstance() {
        if (mySingleton == null) { // 1st check
            lock (myLock) {
                if (mySingleton == null) { // 2nd (double) check
                    mySingleton = new MySingleton();
                    // Write-release semantics are implicitly handled by marking
                    // mySingleton with 'volatile', which inserts the necessary memory
                    // barriers between the constructor call and the write to mySingleton.
                    // The barriers created by the lock are not sufficient because
                    // the object is made visible before the lock is released.
                }
            }
        }
        // The barriers created by the lock are not sufficient because not all threads
        // will acquire the lock. A fence for read-acquire semantics is needed between
        // the test of mySingleton (above) and the use of its contents. This fence
        // is automatically inserted because mySingleton is marked as 'volatile'.
        return mySingleton;
    }
}

En este ejemplo, las tiendas en el MySingletonconstructor podrían no ser visibles para otros procesadores antes de la tienda mySingleton. Si eso sucede, los otros hilos que miran a mySingleton no adquirirán un bloqueo y no necesariamente recogerán las escrituras al constructor.

volatilenunca evita el almacenamiento en caché. Lo que hace es garantizar el orden en que otros procesadores "ven" escribe. Un lanzamiento de tienda retrasará una tienda hasta que se completen todas las escrituras pendientes y se haya emitido un ciclo de bus que indique a otros procesadores que descarten / reescriban su línea de caché si tienen las líneas relevantes almacenadas en caché. Una adquisición de carga vaciará cualquier lectura especulada, asegurando que no serán valores obsoletos del pasado.

doug65536
fuente
Buena explicación. También buen ejemplo de bloqueo de doble verificación. Sin embargo, todavía no estoy seguro de cuándo usarlo, ya que estoy preocupado por los aspectos de almacenamiento en caché. Si escribo una implementación de cola en la que solo 1 hilo escribirá y solo 1 hilo leerá, ¿puedo pasar sin bloqueos y simplemente marcar mis "punteros" de cabeza y cola como volátiles? Quiero asegurarme de que tanto el lector como el escritor vean los valores más actualizados.
nickdu
Ambos heady taildeben ser volátiles para evitar que el productor suponga tailque no cambiará, y para evitar que el consumidor suponga headque no cambiará. Además, headdebe ser volátil para garantizar que las escrituras de datos de la cola sean visibles globalmente antes de que el almacén headsea ​​visible globalmente.
doug65536
+1, los términos como último / "más actualizado", por desgracia, implican un concepto del valor correcto singular. En realidad, dos competidores pueden cruzar una línea de meta exactamente al mismo tiempo ; en una CPU, dos núcleos pueden solicitar una escritura al mismo tiempo . Después de todo, los núcleos no se turnan para hacer el trabajo, eso haría que los núcleos múltiples no tengan sentido. El buen pensamiento / diseño multihilo no debe centrarse en tratar de forzar la "novedad" de bajo nivel, inherentemente falsa, ya que un bloqueo solo fuerza los núcleos a seleccionar arbitrariamente un altavoz a la vez sin equidad, sino más bien tratar de diseñar necesidad de un concepto tan antinatural.
AnorZaken
34

La palabra clave volátil tiene diferentes significados tanto en Java como en C #.

Java

De la especificación del lenguaje Java :

Un campo puede ser declarado volátil, en cuyo caso el modelo de memoria Java asegura que todos los hilos vean un valor consistente para la variable.

C#

De la referencia de C # en la palabra clave volátil :

La palabra clave volátil indica que un campo puede ser modificado en el programa por algo como el sistema operativo, el hardware o un subproceso que se ejecuta simultáneamente.

krock
fuente
Muchas gracias por publicar, como entendí en Java, actúa como bloquear esa variable en un contexto de subproceso, y en C #, si se usa, el valor de la variable se puede cambiar no solo desde el programa, factores externos como el sistema operativo pueden modificar su valor ( sin bloqueo implícito) ... Por favor, avíseme si entendí bien esas diferencias ...
Mircea
@Mircea en Java no implica ningún bloqueo, solo garantiza que se utilizará el valor más actualizado de la variable volátil.
krock
¿Java promete algún tipo de barrera de memoria, o es como C ++ y C # solo prometiendo no optimizar la referencia?
Steven Sudit
La barrera de la memoria es un detalle de implementación. Lo que Java realmente promete es que todas las lecturas verán el valor escrito por la escritura más reciente.
Stephen C
1
@StevenSudit Sí, si el hardware requiere una barrera o carga / adquisición o almacenamiento / liberación, utilizará esas instrucciones. Mira mi respuesta.
doug65536
9

En Java, "volátil" se utiliza para decirle a la JVM que la variable puede ser utilizada por múltiples subprocesos al mismo tiempo, por lo que ciertas optimizaciones comunes no se pueden aplicar.

Cabe destacar la situación en la que los dos hilos que acceden a la misma variable se ejecutan en CPU separadas en la misma máquina. Es muy común que las CPU almacenen en caché de forma agresiva los datos que contiene porque el acceso a la memoria es mucho más lento que el acceso a la memoria caché. Esto significa que si los datos se actualizan en la CPU1, debe pasar inmediatamente por todas las memorias caché y a la memoria principal en lugar de cuando la memoria caché decida borrarse, de modo que la CPU2 pueda ver el valor actualizado (de nuevo ignorando todas las memorias caché en el camino).

Thorbjørn Ravn Andersen
fuente
1

Cuando está leyendo datos que no son volátiles, el hilo de ejecución puede o no siempre obtener el valor actualizado. Pero si el objeto es volátil, el hilo siempre obtiene el valor más actualizado.

Subhash Saini
fuente
1
¿Puedes reformular tu respuesta?
Anirudha Gupta
La palabra clave volátil le dará el valor más actualizado en lugar del valor en caché.
Subhash Saini
0

Volátil es resolver el problema de concurrencia. Para que ese valor esté sincronizado. Esta palabra clave se usa principalmente en un subproceso. Cuando varios hilos actualizan la misma variable.

Shiv Ratan Kumar
fuente
1
No creo que "resuelva" el problema. Es una herramienta que ayuda en alguna circunstancia. No confíe en volátiles para situaciones donde se necesita un bloqueo, como en una condición de carrera.
Scratte