web-development-kb-es.site

¿Cuándo debería usar la palabra clave volátil en C #?

¿Puede alguien proporcionar una buena explicación de la palabra clave volátil en C #? ¿Qué problemas resuelve y cuáles no? ¿En qué casos me ahorrará el uso del bloqueo?

284
Doron Yaacoby

No creo que haya una mejor persona para responder esto que Eric Lippert (énfasis en el original):

En C #, "volátil" significa no solo "asegurarse de que el compilador y la vibración no realicen ningún reordenamiento de código ni registren optimizaciones de almacenamiento en caché en esta variable". También significa "decirle a los procesadores que hagan lo que sea necesario para asegurarse de que estoy leyendo el último valor, incluso si eso significa detener otros procesadores y hacer que sincronicen la memoria principal con sus cachés".

En realidad, ese último bit es una mentira. La verdadera semántica de las lecturas y escrituras volátiles es considerablemente más compleja de lo que he descrito aquí; de hecho en realidad no garantizan que cada procesador detenga lo que está haciendo y actualiza los cachés a/desde la memoria principal. Más bien, proporcionan garantías más débiles sobre cómo se puede observar que los accesos a la memoria antes y después de las lecturas y escrituras se ordenan entre sí . Ciertas operaciones, como crear un nuevo hilo, ingresar un bloqueo o utilizar uno de los métodos de la familia Interlocked, ofrecen garantías más sólidas sobre la observación del pedido. Si desea más detalles, lea las secciones 3.10 y 10.5.3 de la especificación C # 4.0.

Francamente, Te desanimo a que siempre hagas un campo volátil . Los campos volátiles son una señal de que estás haciendo algo totalmente loco: estás intentando leer y escribir el mismo valor en dos hilos diferentes sin poner un candado en su lugar. Los bloqueos garantizan que se observe que la memoria leída o modificada dentro del bloqueo es consistente, los bloqueos garantizan que solo un hilo acceda a una porción determinada de memoria a la vez, y así sucesivamente. El número de situaciones en las que un bloqueo es demasiado lento es muy pequeño, y la probabilidad de que el código sea incorrecto porque no comprende el modelo de memoria exacto es muy grande. No intento escribir ningún código de bloqueo bajo, excepto para los usos más triviales de las operaciones entrelazadas. Dejo el uso de "volátil" a expertos reales.

Para más información ver:

260
Ohad Schneider

Si desea obtener un poco más de información técnica sobre lo que hace la palabra clave volátil, considere el siguiente programa (estoy usando DevStudio 2005):

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

Usando la configuración estándar del compilador optimizado (versión), el compilador crea el siguiente ensamblador (IA32):

void main()
{
00401000  Push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  Push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

Mirando la salida, el compilador ha decidido usar el registro ecx para almacenar el valor de la variable j. Para el bucle no volátil (el primero), el compilador ha asignado i al registro eax. Bastante sencillo. Sin embargo, hay un par de bits interesantes: la instrucción lea ebx, [ebx] es efectivamente una instrucción nop multibyte, de modo que el bucle salta a una dirección de memoria alineada de 16 bytes. El otro es el uso de edx para incrementar el contador de bucles en lugar de usar una instrucción inc eax. La instrucción add reg, reg tiene una latencia más baja en algunos núcleos IA32 en comparación con la instrucción inc reg, pero nunca tiene una latencia más alta.

Ahora para el bucle con el contador de bucle volátil. El contador se almacena en [esp] y la palabra clave volátil le dice al compilador que el valor siempre debe leerse/escribirse en la memoria y nunca asignarse a un registro. El compilador incluso va tan lejos como para no realizar una carga/incremento/almacenamiento como tres pasos distintos (carga eax, inc eax, guardar eax) al actualizar el valor del contador, en lugar de eso, la memoria se modifica directamente en una sola instrucción (un mem adicional). , reg). La forma en que se ha creado el código garantiza que el valor del contador de bucle esté siempre actualizado dentro del contexto de un solo núcleo de CPU. Ninguna operación en los datos puede resultar en corrupción o pérdida de datos (por lo tanto, no se utiliza load/inc/store, ya que el valor puede cambiar durante el inc, por lo que se pierde en la tienda). Dado que las interrupciones solo se pueden reparar una vez que se ha completado la instrucción actual, los datos nunca se pueden corromper, incluso con la memoria no alineada.

Una vez que introduzca una segunda CPU en el sistema, la palabra clave volátil no protegerá los datos que otra CPU actualiza al mismo tiempo. En el ejemplo anterior, necesitaría que los datos no estén alineados para obtener una corrupción potencial. La palabra clave volátil no evitará una posible corrupción si los datos no se pueden manejar de forma atómica, por ejemplo, si el contador de bucles era de tipo largo y largo (64 bits), se necesitarían dos operaciones de 32 bits para actualizar el valor, en medio de que puede producirse una interrupción y cambiar los datos.

Por lo tanto, la palabra clave volátil solo es buena para los datos alineados que son menores o iguales al tamaño de los registros nativos, de modo que las operaciones son siempre atómicas.

La palabra clave volátil fue concebida para ser utilizada con IO operaciones donde IO cambiaría constantemente pero tenía una dirección constante, como una memoria asignada UART dispositivo, y el compilador no debe seguir reutilizando el primer valor leído de la dirección.

Si está manejando grandes volúmenes de datos o tiene varias CPU, entonces necesitará un sistema de bloqueo de nivel superior (OS) para manejar el acceso a los datos correctamente.

55
Skizz

Si está utilizando .NET 1.1, la palabra clave volátil es necesaria cuando se realiza un bloqueo de doble comprobación. ¿Por qué? Debido a que antes de .NET 2.0, el siguiente escenario podría causar que un segundo subproceso acceda a un objeto no nulo, pero no completamente construido:

  1. El hilo 1 pregunta si una variable es nula. //if( este.foo == nulo)
  2. El hilo 1 determina que la variable es nula, por lo que entra en un bloqueo. //lock(this.bar)
  3. El hilo 1 pregunta OTRA VEZ si la variable es nula. //if( este.foo == nulo)
  4. El subproceso 1 aún determina que la variable es nula, por lo que llama a un constructor y asigna el valor a la variable. //this.foo = new Foo ();

Antes de .NET 2.0, se podría asignar a este.foo la nueva instancia de Foo, antes de que el constructor terminara de ejecutarse. En este caso, podría aparecer un segundo hilo (durante la llamada del hilo 1 al constructor de Foo) y experimentar lo siguiente:

  1. El hilo 2 pregunta si la variable es nula. //if( este.foo == nulo)
  2. El hilo 2 determina que la variable NO es nula, así que intenta usarla. //this.foo.MakeFoo ()

Antes de .NET 2.0, podría declarar este.foo como volátil para solucionar este problema. Desde .NET 2.0, ya no necesita usar la palabra clave volátil para lograr un bloqueo de doble comprobación.

Wikipedia en realidad tiene un buen artículo sobre Bloqueo de doble control, y toca brevemente este tema: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

Desde MSDN : el modificador volátil se usa generalmente para un campo al que se accede mediante varios subprocesos sin usar la instrucción de bloqueo para serializar el acceso. El uso del modificador volátil garantiza que un subproceso recupera el valor más actualizado escrito por otro subproceso.

21
Dr. Bob

A veces, el compilador optimizará un campo y usará un registro para almacenarlo. Si el subproceso 1 hace una escritura en el campo y otro subproceso lo accede, ya que la actualización se almacenó en un registro (y no en la memoria), el segundo subproceso obtendría datos obsoletos.

Puede pensar que la palabra clave volátil dice al compilador "Quiero que almacene este valor en la memoria". Esto garantiza que el segundo hilo recupera el último valor.

20
Benoit

A CLR le gusta optimizar las instrucciones, por lo que cuando accede a un campo en el código, es posible que no siempre tenga acceso al valor actual del campo (puede ser de la pila, etc.). Marcar un campo como volatile asegura que la instrucción acceda al valor actual del campo. Esto es útil cuando el valor puede ser modificado (en un escenario sin bloqueo) por un hilo concurrente en su programa o algún otro código que se ejecuta en el sistema operativo.

Obviamente pierdes algo de optimización, pero mantiene el código más simple.

13
Joseph Daigle

Entonces, para resumir todo esto, la respuesta correcta a la pregunta es: si su código se ejecuta en el tiempo de ejecución 2.0 o posterior, la palabra clave volátil casi nunca se necesita y hace más daño que beneficio si se usa innecesariamente. ES DECIR. Nunca lo uses. PERO en versiones anteriores del tiempo de ejecución, IS es necesario para un correcto control doble en los campos estáticos. Específicamente campos estáticos cuya clase tiene código de inicialización de clase estática.

1
Paul Easter

El compilador a veces cambia el orden de las declaraciones en el código para optimizarlo. Normalmente, esto no es un problema en un entorno de un solo subproceso, pero podría ser un problema en un entorno de varios subprocesos. Vea el siguiente ejemplo:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

Si ejecuta t1 y t2, no esperaría ningún resultado o "Valor: 10" como resultado. Podría ser que el compilador cambie de línea dentro de la función t1. Si se ejecuta t2, podría ser que _flag tenga un valor de 5, pero _value tiene 0. Por lo tanto, la lógica esperada podría romperse.

Para solucionar este problema, puede utilizar la palabra clave volatile que puede aplicar al campo. Esta declaración desactiva las optimizaciones del compilador para que pueda forzar el orden correcto en su código.

private static volatile int _flag = 0;

Debe usar volatile solo si realmente lo necesita, ya que deshabilita ciertas optimizaciones del compilador, afectará el rendimiento. Tampoco es compatible con todos los lenguajes .NET (Visual Basic no lo admite), por lo que dificulta la interoperabilidad del idioma.

0
Aliaksei Maniuk