C# - la palabra clave volatile
Tags: csharp, dotnet
Published: 2019-12-02 05:59:38

La palabra clave `volatile` es una de esas palabras clave muy pocas veces comprendidas, la documentación permite concluir que hay que utilizarla siempre que se manejen hilos, pero esto no siempre es así.

Sin embargo lograr identificar que es lo que hace realmente esta palabra clave es una labor complicada así que dedicaré este artículo a explorar esta funcionalidad y a crear un ejemplo práctico que permita entender su verdadera naturaleza.

##La documentación

En __docs__ encontramos la siguiente definición de la palabra clave `volatile`:

> La palabra clave volatile indica que varios subprocesos que se ejecutan a la vez pueden modificar un campo. Los campos que se declaran como volatile no están sujetos a optimizaciones del compilador que suponen el acceso por un subproceso único. Esto garantiza que el valor más actualizado está en todo momento presente en el campo

Revisa acá la [referencia completa](https://docs.microsoft.com/es-es/dotnet/csharp/language-reference/keywords/volatile)

Debemos resaltar dos aspectos importantes de ese texto:

1. Se menciona que los campos `volatile` no son susceptibles de optimizaciones por parte del compilador. **Cuales optimizaciones?**
2. Dice que esto garantiza que el valor más actualizado siempre esta presente en el campo. **No se supone que esto es así siempre?**

A continuación revisaremos estas dos preguntas.

##Optimizaciones del compilador

Siempre que compilamos un programa hecho con C# el compilador se encarga de convertir ese código C# en código de lenguaje __IL__, bueno realmente en OpCodes de IL.

Esto es así de sencillo, pero resulta que cuando compilamos nuestro código en la configuración release o más específicamente cuando se marca la casilla de Optimizar código en el proyecto (ver imagen) el  compilador realiza una revisión general del código par determinar que cosas puede hacer funcionar de una manera mejor a la que codificó el programador inicialmente, o incluso como puede cambiar cosas en el ejecutable que no están en manos del programador ni del propio lenguaje para que el programa sea más eficiente.


![Checkbox Optimizar código](https://thepracticaldev.s3.amazonaws.com/i/0pdi8qv0l7x0jv8ecygu.png)

Usando la línea de comandos podemos también adicionar [optimize](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/optimize-compiler-option) en los modificadores del compilador.

###Ejemplo
`csc t2.cs -optimize`



Estas optimizaciones son calculadas por el compilador haciendo uso de un complejo juego de reglas algunas de las cuales dependen de la arquitectura del procesador ( x86, x64, IA64, etc) y algunas otras del modelo de memoria que se esta trabajando.

>Los modelos de memoria son un tema complejo incluso desde sus fundamentos si bien no es algo imposible de aprender, pero quien quiera puede profundizar un poco más al respecto puede leer el libro de <a href="https://amzn.to/33FgYZa">Sistemas Operativos Modernos de Andrew Tanembaum</a>  y desde luego buscar referencias adicionales en internet.

<a href="https://www.amazon.com/Modern-Operating-Systems-3rd-Edition/dp/0136006639/ref=as_li_ss_il?ie=UTF8&linkCode=li3&tag=juank-20&linkId=4cd0a0a2abd986a6d4bc309a4dde322f&language=es_US" target="_blank"><img border="0" src="//ws-na.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=0136006639&Format=_SL250_&ID=AsinImage&MarketPlace=US&ServiceVersion=20070822&WS=1&tag=juank-20&language=es_US" ></a><img src="https://ir-na.amazon-adsystem.com/e/ir?t=juank-20&language=es_US&l=li3&o=1&a=0136006639" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />


Adicionalmente a las optimizaciones realizadas por el compilador de C# tenemos una segunda etapa de compilación y optimizaciones realizadas por el JIT al momento de ejecutarse el programa, las cuales tiene su propio conjunto de reglas algunas de las cuales también están influenciadas por la arquitectura del procesador y el modelo de memoria.

##El Valor Actualizado

Parte de las optimizaciones realizadas por el compilador pueden evitar que en algún momento determinado los campos de memoria donde se encuentran las variables no sean actualizados con su valor más reciente, porque?

Cada vez que el procesador hace una operación necesita colocar el valor de las variables en uno de sus registros, de tal forma que si tenemos por ejemplo un bucle con una suma de esta manera:

```csharp
int i = 0;
while (condicion)
{
    i = i + 1;
}
```

El procesador enviaría el valor de __i__ a uno de sus registros y el valor __1__ en otro registro, al obtener el valor de la suma el procesador debe colocar este valor de nuevo en memoria en la variable __i__ de tal forma que al continuar el bucle todo el proceso se repite.

Esta secuencia sin embargo puede ser optimizada por el compilador ya que se hace innecesario que el valor de __i__ sea copiado de los registros de la CPU a la memoria y viceversa tantas veces como dure el bucle, así que el compilador genera un código ejecutable que permite que los cálculos sean hechos en su totalidad en los registros de la CPU y solo hasta salir del bucle estos datos serian copiados de nuevo a la memoria.

Este tipo de optimizaciones que usan __cache__  trae una mejora considerable en la ejecución de procesos intensivos a nivel matemático y funcionan bastante bien en la mayoría de los escenarios.

##Optimizaciones de cache y el problema de los hilos

Cuando el compilador esta realizando las optimizaciones analiza todo lo que pueda hacer referencia a las variables en el contexto del __subproceso__ (hilo) actual.

>Realmente en la mayoría de sistemas operativos no se ejecutan los procesos sino los subprocesos y se le llama proceso a un grupo de subprocesos que comparten la memoria.

Con base en lo analizado realiza las optimizaciones, por ello el problema surge cuando un hilo ("nuevo") trata de acceder al valor de una variable que esta siendo modificada en un hilo ("inicial") diferente.

Es posible que el hilo "inicial" se encuentre en  medio de una rutina optimizada para funcionar por __cache__ (en los registros de la CPU) y al no estar copiando el valor de la variable de nuevo en memoria tras cada operación, el hilo "nuevo" no verá los cambios realizados.

Así que en escenarios donde existan múltiples hilos lo ideal es bloquear este tipo de optimizaciones y estar muy atento a los casos donde no sea conveniente inactivarlas pues hay casos para todo.


##Código de ejemplo

Bien después de la teoría vamos a la práctica, veremos ejemplos donde 

1. __volatile__ no sirve para nada y creo que son la mayoría de casos en el mundo real – al menos en CPUs x86 ya que en IA64 al parecer el tema es muy diferente)
2. casos que de acuerdo a la teoría que acabamos de ver funciona perfectamente **desde que lo compiles en modo Release y para x86**.

###Ejemplo 1


Este es el ejemplo típico, donde todos usamos volatile porque así lo dice la documentación pero la realidad es que en estos casos no sirve de NADA.


```csharp
using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
	//volatile
    static int i = 0;
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {
        i++;
        Console.WriteLine("Valor de i="+i);
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}
```

Podríamos poner a __i__ como `volatile` pero no habría diferencia puesto que básicamente el código no necesita ni tiene como ser optimizado. Así que `volatile` no bloquearía ninguna optimización.


###Ejemplo 2


Este es el ejemplo tampoco sirve de NADA, si bien en el bucle se ve la necesidad de optimizar el rendimiento del código evitando que se copie el valor de i o de stopping de los registros de la CPU a la memoria y viceversa.


Resulta que al convertir __i__ a string estamos forzando al compilador a que no utilice el cache dado que necesita el valor mas reciente de __i__ en la memoria para poder llamar el método `ToString()` y al método `WriteLine()`.
Adicionalmente al llamar a alguno de esos métodos se requiere colocar en los registros de la CPU los valores necesarios para cambiar de contexto y para poder ejecutar las operaciones internas de dichos métodos, así que no hay manera de optimizar nada.

```csharp
using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    //volatile
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {
        int i = 0;
 
        while (!stopping)
        {
            i++;
            Console.WriteLine("Valor de i=" + i.ToString());
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}
```

Nuevamente podríamos ponerle a __i__ como `volatile` pero no habría diferencia puesto que básicamente el código no necesita ni tiene como ser optimizado. Así que `volatile` no bloquearía ninguna optimización.

###Ejemplo 3

<div class="alert alert-warning">
En este si que funcionan las optimizaciones! pero el programa no funcionará ya que ambas variables caen en la optimización.
</div>

El compilador optimizara el uso de __i__ para que se apoye en los registros del procesador de principio a fin, eso es perfecto!.

Pero por otro lado la variable __stopping__ también es optimizada, trayendo como resultado que siempre este en uno de los registros y por tanto los hilos *nunca* se enteren en que momento __stopping__ cambio de valor para que saliera del bucle.

```csharp

static class BackgroundTaskDemo
{
    private static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {
        int i = 0;
        Console.WriteLine("Valor de i=" + i.ToString());
        while (!stopping)
        {
            i++;
        }
        Console.WriteLine("Valor de i=" + i.ToString());
        Console.WriteLine("EfectuarTrabajo exit ");
    }
}
```

Al ejecutar la version **release para x86** de este programa veremos como, aunque en código parece que todo saldrá bien, al ejecutarse el programa este nunca terminara pues el hilo nunca se entera que __stopping__ cambió su valor. En pantalla veríamos perpetuamente:

```
Main Exit
```
Y nada más :(.

###Ejemplo 4
Dado que el ejemplo anterior nos muestra como se activan las optimizaciones de código del compilador, es tiempo de evitarlas haciendo uso de volatile.


```csharp

static class BackgroundTaskDemo
{
    private volatile static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {
        int i = 0;
        Console.WriteLine("Valor de i=" + i.ToString());
        while (!stopping)
        {
            i++;
        }
        Console.WriteLine("Valor de i=" + i.ToString());
        Console.WriteLine("EfectuarTrabajo exit ");
    }
}
```

Ahora si en pantalla veremos el resultado esperado
```
Main Exit
EfectuarTrabajo exit
```

:D

###Ejemplo 5
Este es el mismo ejemplo anterior pero un poco mas elaborado haciendo uso de algunas operaciones de System.Math las cuales son suceptibles de optimización por el compilador.

```csharp


using System.Threading;
using System;
 
static class BackgroundTaskDemo
{
    //volatile
    private  static bool stopping = false;
 
    static void Main ()
    {
        new Thread(EfectuarTrabajo).Start();
        Thread.Sleep(5000);
        stopping = true;
 
 
        Console.WriteLine("Main exit");
        Console.ReadLine();
    }
 
    static void EfectuarTrabajo()
    {
        int i = 0;
        int j = 2;
 
        while (!stopping)
        {
            i++;
            j = 100 + (int)Math.Sin((double)i * 10d);
        }
        Console.WriteLine("EfectuarTrabajo exit " + i);
    }
}
```

Prueba quitando y poniendo volatile y verás la diferencia claramente. <br/>
HAS PRUEBAS Y COMPARTE TUS EJEMPLOS

Hasta la próxima!.