30 votos

¿Por qué este código multi-hilo imprime 6 algunas veces?

Crear dos hilos de rosca y pasar a una función que realiza el algoritmo que se muestra a continuación 10.000.000 veces. Sobre todo escribe "5" en la consola, y a veces escribe «3» o «4». Resulta obvio por qué parece ser. Pero, aquí viene la parte confusa: ¿por qué escribe "6" en la consola?

class Program
{
    private static int _state = 3;

    static void Main(string[] args)
    {
        Thread firstThread = new Thread(Tr);
        Thread secondThread = new Thread(Tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

    private static void Tr()
    {
        for (int i = 0; i < 10000000; i++)
        {
            if (_state == 3)
            {
                _state++;
                if (_state != 4)
                {
                    Console.Write(_state);
                }
                _state = 3;
            }
        }
    }
}

Aquí está la salida:enter image description here

40voto

Rob Puntos 2095

Yo creo que he descubierto la secuencia de eventos que conducen a este problema:

Subproceso 1 entra if (_state == 3)

Cambio de contexto

Subproceso 2 entra if (_state == 3)
Subproceso 2 incrementos de estado (state = 4)

Cambio de contexto

Subproceso 1 lee _state como 4

Cambio de contexto

Subproceso 2 conjuntos de _state = 3
Subproceso 2 entra if (_state == 3)

Cambio de contexto

Subproceso 1 ejecuta _state = 4 + 1

Cambio de contexto

Subproceso 2 lee _state como 5
Subproceso 2 ejecuta _state = 5 + 1;

17voto

Paulo Madeira Puntos 1986

Esta es una típica condición de carrera. EDIT: De hecho, hay múltiples condiciones de carrera.

Puede suceder en cualquier momento, _state es 3 y los dos hilos llegar justo después de la if declaración, ya sea simultáneamente a través del cambio de contexto en un solo núcleo, o, simultáneamente, en paralelo en varios núcleos.

Esto es debido a que el ++ operador lee primero _state y luego se incrementa. Es posible que uno se mantenga hasta bastante tiempo después de la primera if declaración de que va a leer 5 o incluso 6.

EDIT: Si quieres generalizar este ejemplo para N hilos, se puede observar un número tan alto como 3 + N+1.

Esto puede ser a la derecha cuando los subprocesos de empezar a correr, o cuando uno acaba de establecer _state 3.

Para evitar esto, utilice un bloqueo alrededor de la if declaración, o el uso de Interlocked acceso _state, como if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3) y System.Threading.Interlocked.Exchange(ref _state, 3).

Si desea mantener la condición de carrera, usted debe declarar _state como volatile, de lo contrario corres el riesgo de cada subproceso ver _state localmente sin necesidad de actualizaciones de los otros hilos.

En alternativa, se puede usar System.Threading.Volatile.Read y System.Threading.Volatile.Write, en caso de que usted cambie su aplicación a tener _state como una variable y Tr como un cierre que captura la variable, como las variables locales no pueden ser (y no será capaz de ser) declaró volatile. En este caso, incluso la inicialización debe ser hecho con un volátil de escritura.


EDIT: tal vez las condiciones de carrera son más evidentes si cambiamos el código ligeramente por la expansión de cada lectura:

    // Without some sort of memory barrier (volatile, lock, Interlocked.*),
    // a thread is allowed to see _state as if other threads hadn't touched it
    private static volatile int _state = 3;

// ...

        for (int i = 0; i < 10000000; i++)
        {
            int currentState;
            currentState = _state;
            if (currentState == 3)
            {
                // RACE CONDITION: re-read the variable
                currentState = _state;
                currentState = currentState + 1:
                // RACE CONDITION: non-atomic write
                _state = currentState;

                currentState = _state;
                if (currentState != 4)
                {
                    // RACE CONDITION: re-read the variable
                    currentState = _state;
                    Console.Write(currentState);
                }
                _state = 3;
            }
        }

He añadido comentarios en los lugares donde _state puede ser diferente de la prevista por la variable anterior sentencias de lectura.

Aquí un diagrama que muestra es incluso posible imprimir 6 dos veces en una fila, una vez en cada subproceso, como la imagen que el op publicado. Recuerde, los hilos no se puede ejecutar en la sincronización, por lo general debido a la preferente de cambio de contexto, caché de puestos, o diferencias en la velocidad (debido al ahorro de energía o temporal de la velocidad turbo):

Race condition prints 6


Esto es similar a la original, pero utiliza la Volatile de la clase, donde state es ahora una variable capturada por un cierre. La cantidad y el orden de la volatilidad de los accesos se hace evidente:

    static void Main(string[] args)
    {
        int state = 3;

        ThreadStart tr = () =>
        {
            for (int i = 0; i < 10000000; i++)
            {
                if (Volatile.Read(ref state) == 3)
                {
                    Volatile.Write(ref state, Volatile.Read(state) + 1);
                    if (Volatile.Read(ref state) != 4)
                    {
                        Console.Write(Volatile.Read(ref state));
                    }
                    Volatile.Write(ref state, 3);
                }
            }
        };

        Thread firstThread = new Thread(tr);
        Thread secondThread = new Thread(tr);

        firstThread.Start();
        secondThread.Start();

        firstThread.Join();
        secondThread.Join();

        Console.ReadLine();
    }

Algunos subprocesos enfoques:

    private static object _lockObject;

// ...

        // Do not allow concurrency, blocking
        for (int i = 0; i < 10000000; i++)
        {
            lock (_lockObject)
            {
                // original code
            }
        }

        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            bool lockTaken = false;
            try
            {
                Monitor.TryEnter(_lockObject, ref lockTaken);
                if (lockTaken)
                {
                    // original code
                }
            }
            finally
            {
                if (lockTaken) Monitor.Exit(_lockObject);
            }
        }


        // Do not allow concurrency, non-blocking
        for (int i = 0; i < 10000000; i++)
        {
            // Only one thread at a time will succeed in exchanging the value
            try
            {
                int previousState = Interlocked.CompareExchange(ref _state, 4, 3);
                if (previousState == 3)
                {
                    // Allow race condition on purpose (for no reason)
                    int currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                    if (currentState != 4)
                    {
                        // This branch is never taken
                        Console.Write(currentState);
                    }
                }
            }
            finally
            {
                Interlocked.CompareExchange(ref _state, 3, 4);
            }
        }


        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState == 4)
            {
                // But still, only one thread at a time enters this branch
                // Allow race condition on purpose (it may actually happen here)
                currentState = Interlocked.CompareExchange(ref _state, 0, 0);
                if (currentState != 4)
                {
                    // This branch might be taken with a maximum value of 3 + N
                    Console.Write(currentState);
                }
            }
            Interlocked.Decrement(ref _state);
        }


Este es un poco diferente, se toma el último valor conocido de _state después de que el incremento de llevar a cabo algo:

        // Allow concurrency
        for (int i = 0; i < 10000000; i++)
        {
            // All threads increment the value
            int currentState = Interlocked.Increment(ref _state);
            if (currentState != 4)
            {
                // Only the thread that incremented 3 will not take the branch
                // This can happen indefinitely after the first increment for N > 1
                // This branch might be taken with a maximum value of 3 + N
                Console.Write(currentState);
            }
            Interlocked.Decrement(ref _state);
        }

Tenga en cuenta que el Interlocked.Increment/Interlocked.Decrement ejemplos no son seguros, a diferencia de la lock/Monitor y Interlocked.CompareExchange ejemplos, ya que no hay una forma confiable de saber si el incremento fue exitosa o no.

Un enfoque común es incrementar, a continuación, siga con un try/finally donde disminución en el finally bloque. Sin embargo, un asincrónica excepción podría ser lanzado (por ejemplo ThreadAbortException)

Asincrónica excepciones que pueden ser lanzadas en lugares insospechados, posiblemente cada instrucción de máquina: ThreadAbortException, StackOverflowException, y OutOfMemoryException.

Otro enfoque es el de inicializar currentState a algo por debajo de 3 y condicionalmente decremento en un finally bloque. Pero, de nuevo, entre Interlocked.Increment regresar y currentState está asignado a el resultado, un asincrónica excepción podría ocurrir, por lo currentState todavía podría tener el valor inicial aunque el Interlocked.Increment éxito.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X