Leí algunos artículos sobre la volatile
palabra clave pero no pude averiguar su uso correcto. ¿Podría decirme para qué se debe usar en C # y en Java?
Leí algunos artículos sobre la volatile
palabra clave pero no pude averiguar su uso correcto. ¿Podría decirme para qué se debe usar en C # y en Java?
Respuestas:
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".
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 i
a 6, la versión optimizada seguirá imprimiendo 5.
La volatile
palabra clave evita dicha optimización y almacenamiento en caché, y por lo tanto es útil cuando una variable puede ser cambiada por otro hilo.
i
marcado como volatile
. En Java, se trata de relaciones antes de pasar .
i
es 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" final
cuando no se declara explícitamente como tal.
Para comprender lo que la volatilidad le hace a una variable, es importante entender qué sucede cuando la variable no es 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.
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.
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 foo
es una variable miembro en una clase, y otras CPU tienen acceso a la instancia del objeto mencionada something
, ¡podrían ver el foo
cambio de valor antes de que las escrituras de memoria en el Thing
constructor 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 foo
es volatile
entonces, la tienda foo
tendrá semántica de lanzamiento, y el hardware garantiza que todas las escrituras antes de la escritura foo
sean visibles para otros procesadores antes de permitir foo
que ocurra la escritura .
¿Cómo es posible que los escritos foo
se reordenen tan mal? Si la retención de línea de caché foo
está 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 MySingleton
constructor 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.
volatile
nunca 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.
head
y tail
deben ser volátiles para evitar que el productor suponga tail
que no cambiará, y para evitar que el consumidor suponga head
que no cambiará. Además, head
debe ser volátil para garantizar que las escrituras de datos de la cola sean visibles globalmente antes de que el almacén head
sea visible globalmente.
La palabra clave volátil tiene diferentes significados tanto en Java como en C #.
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.
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.
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).
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.
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.