¿Cuándo se debe utilizar la palabra clave volátil en C#?

Resuelto Doron Yaacoby asked hace 16 años • 12 respuestas

¿ Alguien puede dar una buena explicación de la volatilepalabra clave en C#? ¿Qué problemas resuelve y cuáles no? ¿En qué casos me ahorrará el uso del bloqueo?

Doron Yaacoby avatar Sep 16 '08 20:09 Doron Yaacoby
Aceptado

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

En C#, "volátil" significa no sólo "asegurarse de que el compilador y el jitter no realicen ningún reordenamiento del 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, eso último es 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, no garantizan que cada procesador detenga lo que está haciendo y actualice los cachés hacia/desde la memoria principal. Más bien, brindan 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 están ordenados entre sí . Ciertas operaciones, como crear un nuevo hilo, ingresar un candado o usar uno de la familia de métodos Interlocked, introducen 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, le desaconsejo que nunca cree un campo volátil . Los campos volátiles son una señal de que estás haciendo algo completamente loco: estás intentando leer y escribir el mismo valor en dos hilos diferentes sin poner un candado. Los bloqueos garantizan que la memoria leída o modificada dentro del bloqueo sea consistente, los bloqueos garantizan que solo un subproceso acceda a una determinada porción de memoria a la vez, y así sucesivamente. La cantidad de situaciones en las que un bloqueo es demasiado lento es muy pequeña y la probabilidad de que obtenga un código 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 operaciones entrelazadas. Dejo el uso de "volátil" a verdaderos expertos.

Para obtener más información, consulte:

  • Comprender el impacto de las técnicas de bloqueo bajo en aplicaciones multiproceso
  • Sayonara volátil
Ohad Schneider avatar Jul 08 '2013 15:07 Ohad Schneider

Si desea ser un poco más técnico 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;
}

Utilizando la configuración estándar optimizada (lanzamiento) del compilador, 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              

Al observar el resultado, el compilador decidió 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 para que el bucle salte a una dirección de memoria alineada de 16 bytes. El otro es el uso de edx para incrementar el contador de bucle 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 mayor.

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 llega a no realizar una carga/incremento/almacenamiento en tres pasos distintos (cargar 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 add mem ,reg.). La forma en que se creó el código garantiza que el valor del contador de bucle esté siempre actualizado dentro del contexto de un único núcleo de CPU. Ninguna operación sobre los datos puede resultar en corrupción o pérdida de datos (por lo tanto, no usar load/inc/store ya que el valor puede cambiar durante el inc y perderse en el almacenamiento). Dado que las interrupciones sólo pueden atenderse una vez que se ha completado la instrucción actual, los datos nunca pueden corromperse, incluso con memoria no alineada.

Una vez que introduce una segunda CPU en el sistema, la palabra clave volátil no protegerá contra la actualización de los datos por parte de otra CPU al mismo tiempo. En el ejemplo anterior, necesitaría que los datos no estuvieran alineados para provocar una posible corrupción. 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 bucle fuera del tipo long long (64 bits), entonces se necesitarían dos operaciones de 32 bits para actualizar el valor, en medio de que puede ocurrir una interrupción y cambiar los datos.

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

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

Si maneja grandes cantidades de datos o tiene varias CPU, necesitará un sistema de bloqueo de nivel superior (OS) para manejar el acceso a los datos correctamente.

Skizz avatar Sep 16 '2008 14:09 Skizz