¿Puede un hilo de C # realmente almacenar en caché un valor e ignorar los cambios a ese valor en otros hilos?

Esta pregunta NO es sobre condiciones de carrera, atomicidad o por qué debería usar lockings en su código. Yo ya sé acerca de esos.

ACTUALIZACIÓN: Mi pregunta no es “¿Existe rareza con memoria volátil?” (Sé que sí), mi pregunta es “¿el runtime de .NET no lo elimina para que nunca lo veas?”.

Consulte http://www.yoda.arachsys.com/csharp/threads/volatility.shtml y la primera respuesta en ¿Es una propiedad de cadena en sí misma segura?

(Son realmente el mismo artículo ya que uno hace referencia al otro). Un hilo establece un bool y el otro hilo repite para siempre la lectura del bool: esos artículos afirman que el hilo de lectura podría almacenar en caché el valor anterior y nunca leer el nuevo valor, por lo por lo tanto, necesita un locking (o use la palabra clave volátil). Afirman que el siguiente código potencialmente bucle para siempre. Ahora estoy de acuerdo en que es una buena práctica bloquear sus variables, pero no puedo creer que el tiempo de ejecución de .NET realmente ignore el cambio en el valor de la memoria como lo afirma el artículo. Entiendo su charla sobre la memoria volátil frente a la memoria no volátil, y estoy de acuerdo en que tienen un punto válido en el código no administrado , pero no puedo creer que el tiempo de ejecución de .NET no lo abstraiga correctamente para que el siguiente código lo haga que esperas. El artículo incluso admite que el código funcionará “casi con certeza” (aunque no está garantizado), así que llamo a BS en el reclamo. ¿Alguien puede verificar que es cierto que el siguiente código no siempre funcionará? ¿Alguien puede obtener un solo caso (quizás no siempre se puede reproducir) donde esto falla?

class BackgroundTaskDemo { private bool stopping = false; static void Main() { BackgroundTaskDemo demo = new BackgroundTaskDemo(); new Thread(demo.DoWork).Start(); Thread.Sleep(5000); demo.stopping = true; } static void DoWork() { while (!stopping) { // Do something here } } } 

El punto es: podría funcionar, pero no está garantizado que funcione según la especificación. Normalmente lo que las personas buscan es un código que funciona por las razones correctas, en lugar de trabajar con una combinación fluke del comstackdor, el tiempo de ejecución y el JIT, que puede cambiar entre las versiones de framework, la CPU física, la plataforma y cosas como x86 vs x64.

Comprender el modelo de memoria es un área muy compleja, y no pretendo ser un experto; pero las personas que son verdaderos expertos en esta área me aseguran que el comportamiento que estás viendo no está garantizado.

Puede publicar tantos ejemplos de trabajo como desee, pero desafortunadamente eso no demuestra mucho más que “normalmente funciona”. Ciertamente no prueba que esté garantizado que funcione. Solo se necesitaría un contraejemplo único para refutar, pero encontrarlo es el problema …

No, no tengo uno a mano.


Actualización con contraejemplo repetible:

 using System.Threading; using System; static class BackgroundTaskDemo { // make this volatile to fix it private static bool stopping = false; static void Main() { new Thread(DoWork).Start(); Thread.Sleep(5000); stopping = true; Console.WriteLine("Main exit"); Console.ReadLine(); } static void DoWork() { int i = 0; while (!stopping) { i++; } Console.WriteLine("DoWork exit " + i); } } 

Salida:

 Main exit 

pero aún en ejecución, a plena CPU; tenga en cuenta que la stopping se ha establecido en true en este punto. ReadLine es para que el proceso no finalice. La optimización parece depender del tamaño del código dentro del ciclo (por lo tanto, i++ ). Solo funciona en el modo “liberar” obviamente. Agregue volatile y todo funciona bien.

Este ejemplo incluye el código x86 nativo como comentarios para demostrar que la variable de control (‘stopLooping’) está en caché.

Cambia ‘stopLooping’ a volátil para “arreglarlo”.

Esto se creó con Visual Studio 2008 como versión de lanzamiento y se ejecuta sin depuración.

  using System; using System.Threading; /* A simple console application which demonstrates the need for the volatile keyword and shows the native x86 (JITed) code.*/ static class LoopForeverIfWeLoopOnce { private static bool stopLooping = false; static void Main() { new Thread(Loop).Start(); Thread.Sleep(1000); stopLooping = true; Console.Write("Main() is waiting for Enter to be pressed..."); Console.ReadLine(); Console.WriteLine("Main() is returning."); } static void Loop() { /* * Stack frame setup (Native x86 code): * 00000000 push ebp * 00000001 mov ebp,esp * 00000003 push edi * 00000004 push esi */ int i = 0; /* * Initialize 'i' to zero ('i' is in register edi) * 00000005 xor edi,edi */ while (!stopLooping) /* * Load 'stopLooping' into eax, test and skip loop if != 0 * 00000007 movzx eax,byte ptr ds:[001E2FE0h] * 0000000e test eax,eax * 00000010 jne 00000017 */ { i++; /* * Increment 'i' * 00000012 inc edi */ /* * Test the cached value of 'stopped' still in * register eax and do it again if it's still * zero (false), which it is if we get here: * 00000013 test eax,eax * 00000015 je 00000012 */ } Console.WriteLine("i={0}", i); } } 

FWIW:

  • He visto esta optimización del comstackdor del comstackdor MS C ++ (código no administrado).
  • No sé si sucede en C #
  • No sucederá durante la depuración (las optimizaciones del comstackdor se desactivan automáticamente durante la depuración)
  • Incluso si esa optimización no ocurre ahora, está apostando a que nunca introducirán esa optimización en versiones futuras del comstackdor JIT.