Por qué necesitamos Hilo.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 una locura. ¿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.

¿Se molesta en usar barreras?

Author: Adi Lester, 2010-08-24

6 answers

Le va a costar mucho reproducir este bug. De hecho, iría tan lejos como para decir que nunca podrá reproducirlo usando. NET Framework. La razón es porque la implementación de Microsoft utiliza un modelo de memoria fuerte para las escrituras. Eso significa que las escrituras son tratadas como si fueran volátiles. Una escritura volátil tiene semántica de liberación de bloqueo, lo que significa que todas las escrituras anteriores deben confirmarse antes de la escritura actual.

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

Así que lo que estoy diciendo es que es muy poco probable que la eliminación de las barreras #1 y #2 tenga algún impacto en el comportamiento del programa. Eso, por supuesto, no es una garantía, sino una observación basada únicamente en la aplicación actual del CLR.

Eliminar las barreras #3 y #4 definitivamente tendrá un impacto. Esto es realmente 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 compilado usando la compilación de Release y ejecutado fuera del depurador. El error es que el programa no termina. Puede corregir el error colocando una llamada a Thread.MemoryBarrier dentro del bucle 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 que algunos errores de enhebrado son difíciles de reproducir es porque las mismas tácticas que usa para simular el entrelazado de hilos en realidad pueden corregir la error. Thread.Sleep es el ejemplo más notable porque genera barreras de memoria. Puede verificar eso colocando una llamada dentro del bucle while y observando que el error desaparece.

Puedes ver mi respuesta aquí para otro análisis de ejemplo en el libro citado.

 64
Author: Brian Gideon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:19

Las probabilidades son muy bueno que la primera tarea se completa en el momento en que la 2a tarea incluso comienza a ejecutarse. Solo puede observar este comportamiento si ambos subprocesos ejecutan ese código simultáneamente y no hay operaciones de sincronización de caché intervinientes. Hay uno en tu código, el método StartNew() tomará un bloqueo dentro del administrador del grupo de subprocesos en alguna parte.

Conseguir dos hilos para ejecutar este código simultáneamente es muy difícil. Este código se completa en un par de nanosegundos. Tendrías que probar miles de millones de veces e introducir retrasos variables para tener probabilidades. No tiene mucho sentido para esto, por supuesto, el verdadero problema es cuando esto sucede aleatoriamente cuando no lo espera.

Manténgase alejado de esto, use la instrucción lock para escribir código multihilo sano.

 10
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-24 12:48:33

Si utiliza volatile y lock, la barrera de memoria está incorporada. Pero, sí, lo necesitas de otra manera. Dicho esto, sospecho que necesita la mitad de lo que muestra su ejemplo.

 2
Author: Steven Sudit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-24 12:28:16

Es muy difícil reproducir errores multiproceso - por lo general, tiene que ejecutar el código de prueba muchas veces (miles) y tener alguna comprobación automatizada que marcará si se produce el error. Podrías intentar añadir un hilo corto.Sleep(10) entre algunas de las líneas, pero de nuevo no siempre garantiza que obtendrá los mismos problemas que sin él.

Se introdujeron barreras de memoria para las personas que necesitan hacer una optimización de rendimiento de bajo nivel realmente hardcore de su código multiproceso. En en la mayoría de los casos, estará mejor cuando use otras primitivas de sincronización, es decir, volátiles o bloqueadas.

 2
Author: Grzenio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-24 12:31:40

Citaré uno de los grandes 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 hilos diferentes, podría ser ¿es posible que B escriba "0"? La respuesta es sí - para lo siguiente razones:

El compilador, CLR o CPU puede reordenar las instrucciones de su programa para mejorar la eficiencia. El compilador, CLR o CPU puede introducir el almacenamiento en caché optimizaciones tales que las asignaciones a variables no serán visibles para otros hilos de inmediato. C# y el tiempo de ejecución son muy cuidadosos para asegúrese de que tales optimizaciones no rompan el hilo simple ordinario código - o código multiproceso que hace un uso adecuado de los bloqueos. Fuera de estos escenarios, debe derrotar explícitamente estas optimizaciones por crear barreras de memoria (también llamadas cercas de memoria) para limitar efectos del reordenamiento de instrucciones y del almacenamiento en caché de lectura/escritura.

Vallas completas

El tipo más simple de la barrera de la memoria es una memoria completa barrera (valla completa) que evita cualquier tipo de reordenamiento de instrucciones o escondiéndote alrededor de esa cerca. Llamando a Hilo.MemoryBarrier genera un valla completa; podemos arreglar nuestro ejemplo aplicando cuatro vallas completas como sigue:

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é necesitamos usarla en escenarios sin bloqueo para hacer que el código sea seguro y robusto se describe muy bien aquí: http://www.albahari.com/threading/part4.aspx

 1
Author: Łukasz Szkup,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-21 05:19:48

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

Siempre uso algún tipo de herramienta de nivel superior como un monitor o un candado, pero internamente son hacer algo similar o se implementan con barreras.

 0
Author: dsolimano,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-24 12:34:41