¿Por qué necesitamos Thread.MemoryBarrier ()?

En “C # 4 in a Nutshell”, el autor muestra que esta clase puede escribir 0 a veces sin MemoryBarrier , aunque no puedo reproducir en mi Core2Duo:

 public class Foo { int _answer; bool _complete; public void A() { _answer = 123; //Thread.MemoryBarrier(); // Barrier 1 _complete = true; //Thread.MemoryBarrier(); // Barrier 2 } public void B() { //Thread.MemoryBarrier(); // Barrier 3 if (_complete) { //Thread.MemoryBarrier(); // Barrier 4 Console.WriteLine(_answer); } } } private static void ThreadInverteOrdemComandos() { Foo obj = new Foo(); Task.Factory.StartNew(obj.A); Task.Factory.StartNew(obj.B); Thread.Sleep(10); } 

Esta necesidad me parece loca. ¿Cómo puedo reconocer todos los casos posibles que esto puede ocurrir? Creo que si el procesador cambia el orden de las operaciones, debe garantizar que el comportamiento no cambie.

¿Te molestas en usar Barriers?

Va a ser muy difícil reproducir este error. De hecho, iría tan lejos como para decir que nunca serás capaz de reproducirlo usando .NET Framework. La razón es porque la implementación de Microsoft usa un fuerte modelo de memoria para escrituras. Eso significa que las escrituras son tratadas como si fueran volátiles. Una escritura volátil tiene semántica de liberación de locking, lo que significa que todas las escrituras previas deben confirmarse antes de la escritura actual.

Sin embargo, la especificación ECMA tiene un modelo de memoria más débil. Por lo tanto, es teóricamente posible que Mono o incluso una versión futura de .NET Framework comience a exhibir el comportamiento erróneo.

Entonces, lo que estoy diciendo es que es muy poco probable que la eliminación de las barreras n. ° 1 y n. ° 2 tenga un impacto en el comportamiento del progtwig. Eso, por supuesto, no es una garantía, sino una observación basada en la implementación actual del CLR solamente.

La eliminación de las barreras n. ° 3 y n. ° 4 definitivamente tendrá un impacto. Esto es bastante fácil de reproducir. Bueno, no este ejemplo per se, pero el siguiente código es una de las demostraciones más conocidas. Tiene que ser comstackdo usando la comstackción Release y se ejecutó fuera del depurador. El error es que el progtwig no termina. Puede solucionar el error haciendo una llamada a Thread.MemoryBarrier dentro del ciclo while o marcando stop como volatile .

 class Program { static bool stop = false; public static void Main(string[] args) { var t = new Thread(() => { Console.WriteLine("thread begin"); bool toggle = false; while (!stop) { toggle = !toggle; } Console.WriteLine("thread end"); }); t.Start(); Thread.Sleep(1000); stop = true; Console.WriteLine("stop = true"); Console.WriteLine("waiting..."); t.Join(); } } 

La razón por la cual algunos errores de enhebrado son difíciles de reproducir es porque las mismas tácticas que usas para simular el entrelazado de hilos en realidad pueden arreglar el error. Thread.Sleep es el ejemplo más notable porque genera barreras de memoria. Puede verificarlo haciendo una llamada dentro del ciclo while y observando que el error desaparece.

Puedes ver mi respuesta aquí para otro análisis del ejemplo del libro que citaste.

Las probabilidades son muy buenas de que la primera tarea se complete para el momento en que la segunda tarea incluso comience a ejecutarse. Solo puede observar este comportamiento si ambos subprocesos ejecutan ese código simultáneamente y no hay operaciones intermedias de sincronización de caché. Hay uno en su código, el método StartNew () tendrá un locking dentro del administrador de grupo de subprocesos en alguna parte.

Obtener dos hilos para ejecutar este código simultáneamente es muy difícil. Este código se completa en un par de nanosegundos. Tendría que probar miles de millones de veces e introducir retrasos variables para tener probabilidades. No hay mucho que señalar a esto, por supuesto, el verdadero problema es cuando esto sucede al azar cuando no lo esperas.

Aléjese de esto, use la instrucción de locking para escribir un código de varios subprocesos.

Si usa volatile y lock , la barrera de memoria está incorporada. Pero, sí, lo necesita de otra manera. Habiendo dicho eso, sospecho que necesitas la mitad de lo que muestra tu ejemplo.

Es muy difícil reproducir errores de subprocesos múltiples; por lo general, debe ejecutar el código de prueba muchas veces (miles) y tener algún control automático que marcará si se produce el error. Puede intentar agregar un Thread.Sleep (10) corto entre algunas de las líneas, pero nuevamente no siempre garantiza que obtendrá los mismos problemas que sin él.

Las barreras de memoria se introdujeron para las personas que necesitan hacer una optimización del rendimiento de bajo nivel realmente dura de su código multiproceso. En la mayoría de los casos, será mejor cuando utilice otras primitivas de sincronización, es decir, volátiles o de locking.

Voy a citar uno de los excelentes artículos sobre multi-threading:

Considere el siguiente ejemplo:

 class Foo { int _answer; bool _complete; void A() { _answer = 123; _complete = true; } void B() { if (_complete) Console.WriteLine (_answer); } } 

Si los métodos A y B se ejecutan simultáneamente en diferentes hilos, ¿podría ser posible que B escriba “0”? La respuesta es sí, por los siguientes motivos:

El comstackdor, el CLR o la CPU pueden reordenar las instrucciones de su progtwig para mejorar la eficiencia. El comstackdor, el CLR o la CPU pueden introducir optimizaciones del almacenamiento en caché de manera que las asignaciones a las variables no sean visibles para otros hilos de inmediato. C # y el tiempo de ejecución son muy cuidadosos para garantizar que dichas optimizaciones no rompan el código ordinario de un solo subproceso, o el código multiproceso que hace un uso adecuado de los lockings. Fuera de estos escenarios, debe vencer explícitamente estas optimizaciones creando barreras de memoria (también llamadas vallas de memoria) para limitar los efectos del reordenamiento de la instrucción y el almacenamiento en caché de lectura / escritura.

Vallas completas

El tipo más simple de barrera de memoria es una barrera de memoria completa (valla completa) que evita cualquier tipo de instrucción de reordenamiento o almacenamiento en caché alrededor de esa valla. Calling Thread.MemoryBarrier genera una valla completa; podemos arreglar nuestro ejemplo aplicando cuatro vallas completas de la siguiente manera:

 class Foo { int _answer; bool _complete; void A() { _answer = 123; Thread.MemoryBarrier(); // Barrier 1 _complete = true; Thread.MemoryBarrier(); // Barrier 2 } void B() { Thread.MemoryBarrier(); // Barrier 3 if (_complete) { Thread.MemoryBarrier(); // Barrier 4 Console.WriteLine (_answer); } } } 

Toda la teoría detrás de Thread.MemoryBarrier y por qué tenemos que usarla en escenarios sin locking para hacer que el código sea seguro y robusto se describe muy bien aquí: http://www.albahari.com/threading/part4.aspx

Si alguna vez tocas datos de dos hilos diferentes, esto puede ocurrir. Este es uno de los trucos que los procesadores usan para boost la velocidad: podrías construir procesadores que no hicieran esto, pero serían mucho más lentos, por lo que ya nadie lo hace. Probablemente deberías leer algo como Hennessey y Patterson para reconocer todos los diversos tipos de condiciones de carrera.

Siempre utilizo algún tipo de herramienta de nivel superior, como un monitor o un candado, pero internamente están haciendo algo similar o están implementados con barreras.