Ilustrando el uso de la palabra clave volátil en C #

88

Me gustaría codificar un pequeño programa que ilustre visualmente el comportamiento de la volatilepalabra clave. Idealmente, debería ser un programa que realice un acceso concurrente a un campo estático no volátil y que tenga un comportamiento incorrecto debido a eso.

Agregar la palabra clave volátil en el mismo programa debería solucionar el problema.

Eso es algo que no logré lograr. Incluso intentándolo varias veces, habilitando la optimización, etc., siempre obtengo un comportamiento correcto sin la palabra clave 'volátil'.

¿Tienes alguna idea sobre este tema? ¿Sabe cómo simular un problema de este tipo en una aplicación de demostración sencilla? ¿Depende del hardware?

Romain Verdier
fuente

Respuestas:

102

¡He logrado un ejemplo de trabajo!

La idea principal recibida de wiki, pero con algunos cambios para C #. El artículo de wiki demuestra esto para el campo estático de C ++, parece que C # siempre compila cuidadosamente las solicitudes en campos estáticos ... y hago un ejemplo con uno no estático:

Si ejecuta este ejemplo en modo Release y sin depurador (es decir, usando Ctrl + F5), la línea while (test.foo != 255)se optimizará a 'while (true)' y este programa nunca regresará. Pero después de agregar una volatilepalabra clave, siempre obtiene "OK".

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}
S. Serpooshan
fuente
5
Probado en .NET 4.0, tanto x86 como x64, puede confirmar que el ejemplo sigue siendo aplicable. ¡Gracias! :)
Roman Starkov
1
¡Eso es bueno! Nunca supe que el JIT hace este tipo de optimización y que volátil lo desactiva.
usr
3
Para ser explícito, está agregando la palabra clave "volatile" a la declaración de foo, ¿verdad?
JoeCool
21

Sí, depende del hardware (es poco probable que vea el problema sin varios procesadores), pero también depende de la implementación. Las especificaciones del modelo de memoria en la especificación CLR permiten cosas que la implementación de Microsoft del CLR no necesariamente hace.

Craig Stuntz
fuente
6

No es realmente una cuestión de que ocurra una falla cuando no se especifica la palabra clave 'volátil', sino que podría ocurrir un error cuando no se ha especificado. En general, ¡sabrá cuando este es el caso mejor que el compilador!

La forma más sencilla de pensar en ello sería que el compilador podría, si quisiera, incorporar ciertos valores. Al marcar el valor como volátil, se está diciendo a sí mismo y al compilador que el valor puede cambiar (incluso si el compilador no lo cree así). Esto significa que el compilador no debe incluir valores en línea, mantener la caché o leer el valor antes (en un intento de optimizar).

Este comportamiento no es realmente la misma palabra clave que en C ++.

MSDN tiene una breve descripción aquí . Aquí hay una publicación quizás más profunda sobre los temas de volatilidad, atomicidad y enclavamiento.

Ray Hayes
fuente
4

Es difícil de demostrar en C #, ya que el código es abstraído por una máquina virtual, por lo que en una implementación de esta máquina funciona correctamente sin volátiles, mientras que puede fallar en otra.

Sin embargo, Wikipedia tiene un buen ejemplo de cómo demostrarlo en C.

Lo mismo podría suceder en C # si el compilador JIT decide que el valor de la variable no puede cambiar de todos modos y, por lo tanto, crea un código de máquina que ya ni siquiera lo verifica. Si ahora otro hilo estaba cambiando el valor, su primer hilo aún podría estar atrapado en el bucle.

Otro ejemplo es Ocupado esperando.

Nuevamente, esto también podría suceder con C #, pero depende en gran medida de la máquina virtual y del compilador JIT (o intérprete, si no tiene JIT ... en teoría, creo que MS siempre usa un compilador JIT y también Mono usa uno; pero es posible que pueda desactivarlo manualmente).

Mecki
fuente
4

Aquí está mi contribución a la comprensión colectiva de este comportamiento ... No es mucho, solo una demostración (basada en la demostración de xkip) que muestra el comportamiento de un valor de int no volátil (es decir, "normal"), en paralelo -side, en el mismo programa ... que es lo que estaba buscando cuando encontré este hilo.

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

Hay algunas explicaciones sucintas realmente excelentes arriba, así como algunas referencias geniales. Gracias a todos por ayudarme a entender volatile(al menos lo suficiente como para saber que no depende de volatiledónde fue mi primer instinto lock).

Saludos y gracias por TODOS los peces. Keith.


PD: Me interesaría mucho una demostración de la solicitud original, que era: "Me gustaría ver un int estático volátil comportándose correctamente donde un int estático se comporta mal.

Lo intenté y fracasé en este desafío. (De hecho, me di por vencido bastante rápido ;-). En todo lo que probé con variables estáticas, se comportan "correctamente" independientemente de si son volátiles o no ... y me encantaría una explicación de POR QUÉ ese es el caso, si es que es el caso ... ¿Es eso? el compilador no almacena en caché los valores de las variables estáticas en los registros (es decir, almacena en caché una referencia a esa dirección del montón)

No, esta no es una pregunta nueva ... es un intento de devolver a la comunidad a la pregunta original.

Corlettk
fuente
2

Me encontré con el siguiente texto de Joe Albahari que me ayudó mucho.

Tomé un ejemplo del texto anterior que modifiqué un poco, creando un campo volátil estático. Cuando quita la volatilepalabra clave, el programa se bloqueará indefinidamente. Ejecute este ejemplo en modo de lanzamiento .

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}
Martijn B
fuente