Cuándo usar la palabra clave volátil en C #

Las técnicas de optimización utilizadas por el compilador JIT (just-in-time) en Common Language Runtime pueden generar resultados impredecibles cuando su programa .Net intenta realizar lecturas de datos no volátiles en un escenario multiproceso. En este artículo veremos las diferencias entre el acceso a la memoria volátil y no volátil, el papel de la palabra clave volátil en C # y cómo se debe usar la palabra clave volátil.

Proporcionaré algunos ejemplos de código en C # para ilustrar los conceptos. Para comprender cómo funciona la palabra clave volátil, primero debemos comprender cómo funciona la estrategia de optimización del compilador JIT en .Net.

Comprender las optimizaciones del compilador JIT

Cabe señalar que el compilador JIT, como parte de una estrategia de optimización, cambiará el orden de las lecturas y escrituras de una manera que no cambie el significado y la salida final del programa. Esto se ilustra en el fragmento de código que se proporciona a continuación.

x = 0;

x = 1;

El fragmento de código anterior se puede cambiar a lo siguiente, conservando la semántica original del programa.

x = 1;

El compilador JIT también puede aplicar un concepto llamado "propagación constante" para optimizar el siguiente código.

x = 1;

y = x;

El fragmento de código anterior se puede cambiar a lo siguiente, de nuevo conservando la semántica original del programa.

x = 1;

y = 1;

Acceso a memoria volátil frente a no volátil

El modelo de memoria de los sistemas modernos es bastante complicado. Tiene registros de procesador, varios niveles de cachés y memoria principal compartida por varios procesadores. Cuando su programa se ejecuta, el procesador puede almacenar en caché los datos y luego acceder a estos datos desde el caché cuando lo solicite el subproceso en ejecución. Las actualizaciones y lecturas de estos datos pueden ejecutarse en la versión en caché de los datos, mientras que la memoria principal se actualiza en un momento posterior. Este modelo de uso de la memoria tiene consecuencias para las aplicaciones multiproceso. 

Cuando un subproceso está interactuando con los datos en la memoria caché y un segundo subproceso intenta leer los mismos datos al mismo tiempo, el segundo subproceso puede leer una versión desactualizada de los datos de la memoria principal. Esto se debe a que cuando se actualiza el valor de un objeto no volátil, el cambio se realiza en la caché del hilo en ejecución y no en la memoria principal. Sin embargo, cuando se actualiza el valor de un objeto volátil, no solo se realiza el cambio en el caché del subproceso en ejecución, sino que este caché se vacía en la memoria principal. Y cuando se lee el valor de un objeto volátil, el hilo actualiza su caché y lee el valor actualizado.

Usando la palabra clave volátil en C #

La palabra clave volátil en C # se usa para informar al compilador JIT que el valor de la variable nunca debe almacenarse en caché porque podría ser cambiado por el sistema operativo, el hardware o un subproceso que se ejecuta simultáneamente. Por lo tanto, el compilador evita el uso de optimizaciones en la variable que puedan conducir a conflictos de datos, es decir, a que diferentes hilos accedan a diferentes valores de la variable.

Cuando marca un objeto o una variable como volátil, se convierte en un candidato para lecturas y escrituras volátiles. Cabe señalar que en C # todas las escrituras de memoria son volátiles independientemente de si está escribiendo datos en un objeto volátil o no volátil. Sin embargo, la ambigüedad ocurre cuando lee datos. Cuando está leyendo datos que no son volátiles, el subproceso en ejecución puede obtener o no siempre el último valor. Si el objeto es volátil, el hilo siempre obtiene el valor más actualizado.

Puede declarar una variable como volátil precediéndola con la volatilepalabra clave. El siguiente fragmento de código ilustra esto.

programa de clase

    {

        public volatile int i;

        static void Main (cadena [] argumentos)

        {

            // Escribe tu código aquí

        }

    }

Puede utilizar la volatilepalabra clave con cualquier tipo de referencia, puntero y enumeración. También puede utilizar el modificador volátil con los tipos byte, short, int, char, float y bool. Cabe señalar que las variables locales no se pueden declarar como volátiles. Cuando especifica un objeto de tipo de referencia como volátil, solo el puntero (un entero de 32 bits que apunta a la ubicación en la memoria donde el objeto está realmente almacenado) es volátil, no el valor de la instancia. Además, una variable doble no puede ser volátil porque tiene un tamaño de 64 bits, mayor que el tamaño de la palabra en los sistemas x86. Si necesita hacer que una variable doble sea volátil, debe envolverla en clase. Puede hacer esto fácilmente creando una clase contenedora como se muestra en el fragmento de código a continuación.

clase pública VolatileDoubleDemo

{

    private volatile WrappedVolatileDouble volatileData;

}

clase pública WrappedVolatileDouble

{

    Public double Data {get; conjunto; }

Sin embargo, tenga en cuenta la limitación del ejemplo de código anterior. Aunque tendría el último valor del volatileDatapuntero de referencia, no se le garantiza el último valor de la Datapropiedad. La solución para esto es hacer que el WrappedVolatileDoubletipo sea inmutable.

Aunque la palabra clave volátil puede ayudarlo con la seguridad de los subprocesos en ciertas situaciones, no es una solución para todos sus problemas de concurrencia de subprocesos. Debe saber que marcar una variable o un objeto como volátil no significa que no necesite usar la palabra clave de bloqueo. La palabra clave volátil no sustituye a la palabra clave de bloqueo. Solo está ahí para ayudarlo a evitar conflictos de datos cuando tiene varios subprocesos que intentan acceder a los mismos datos.