32 votes

Pourquoi ce code multithread affiche-t-il 6 fois?

Je crée deux threads et passe une fonction qui exécute l'algorithme indiqué ci-dessous 10 000 000 fois. Généralement, il écrit "5" dans la console, et parfois il écrit "3" ou "4". Il est assez évident pourquoi cela semble être le cas. Mais, voici la partie déroutante: pourquoi écrit-il "6" dans la console?

 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;
            }
        }
    }
}
 

Voici la sortie: entrez la description de l'image ici

44voto

Rob Points 2095

Je pense que j'ai compris la séquence des événements menant à cette question:

Fil entre 1 if (_state == 3)

Changement de contexte

Fil entre 2 if (_state == 3)
Thread 2 incréments d'état (state = 4)

Changement de contexte

Thread 1 lit _state comme 4

Changement de contexte

Thread 2 ensembles _state = 3
Fil entre 2 if (_state == 3)

Changement de contexte

1 Thread exécute _state = 4 + 1

Changement de contexte

Thread 2 lectures _state comme 5
Thread 2 exécute _state = 5 + 1;

17voto

Paulo Madeira Points 1986

Il s'agit d'une condition de course. EDIT: En fait, il existe plusieurs conditions de course.

Il peut arriver à tout moment en cas _state est de 3 et les deux fils d'atteindre juste au-delà de l' if déclaration, soit en même temps grâce à la commutation de contexte dans un seul core, simultanément ou en parallèle dans plusieurs cœurs.

C'est parce que l' ++ opérateur de lit pour la première fois _state et l'incrémente. Il est possible que l'un a eu assez de temps, après la première if déclaration qu'il va lire 5 ou même 6.

EDIT: Si vous voulez généraliser cet exemple pour N threads, vous pouvez observer un nombre aussi élevé que 3 + N+1.

Ce peut être le droit lorsque les fils commencent à courir, ou lorsque l'on vient de mettre en _state à 3.

Pour éviter cela, utilisez un verrou autour de l' if déclaration, ou utiliser Interlocked accès _state, comme if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3) et System.Threading.Interlocked.Exchange(ref _state, 3).

Si vous voulez garder la condition de la course, vous devez déclarer _state comme volatile, sinon vous risquez de chaque thread voir _state localement, sans les mises à jour des autres threads.

En alternative, vous pouvez utiliser System.Threading.Volatile.Read et System.Threading.Volatile.Write, dans le cas où vous changez de mise en œuvre de disposer _state comme une variable et d' Tr comme une fermeture qui capte cette variable, ainsi que les variables locales peuvent pas (et ne sera pas en mesure d'être) a déclaré, volatile. Dans ce cas, même l'initialisation doit être fait avec une écriture volatile.


EDIT: peut-être les conditions de course sont de plus en plus apparente si l'on change le code légèrement par l'expansion de tous les lire:

    // 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;
            }
        }

J'ai ajouté des commentaires dans les endroits où l' _state peuvent être différentes de celles assumées par les précédents variable de lire une déclaration.

Voici un long diagramme, ce qui montre qu'il est même possible d'imprimer 6 deux fois dans une rangée, une fois dans chaque thread, comme l'image que l' op posté. Rappelez-vous, les threads ne peuvent pas s'exécuter dans synch, généralement en raison de préemption de commutation de contexte, la mise en cache des stalles, le cœur des différences de vitesse (en raison d'économie d'énergie ou temporaire de la vitesse turbo):

Race condition prints 6


Celui-ci est semblable à l'original, mais il utilise l' Volatile classe, où l' state est maintenant une variable capturée par une fermeture. Le montant et l'ordre de la volatilité des accès devient évident:

    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();
    }

Certains thread-safe approches:

    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);
        }


Celui-ci est un peu différent, il prend la dernière valeur connue de l' _state après l'incrémentation effectuer quelque chose:

        // 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);
        }

Notez que l' Interlocked.Increment/Interlocked.Decrement exemples ne sont pas sûrs, contrairement à l' lock/Monitor et Interlocked.CompareExchange exemples, comme il n'y a aucun moyen fiable de savoir si l'augmentation a été couronnée de succès ou non.

Une approche courante consiste à incrémenter, puis suivre avec un try/finally où vous décrémenter dans l' finally bloc. Cependant, asynchrone exception peut être levée (par exemple, ThreadAbortException)

Asynchrone des exceptions peuvent être jetés dans des lieux inattendus, chaque instruction machine: ThreadAbortException, StackOverflowException, et OutOfMemoryException.

Une autre approche consiste à initialiser currentState de quelque chose en dessous de 3, et à condition que la décrémentation en finally bloc. Mais encore une fois, dans l'entre - Interlocked.Increment de la retourner et de currentState être attribuée au résultat, asynchrone exception peut se produire, si l' currentState pourrait encore avoir de la valeur initiale, même si l' Interlocked.Increment réussi.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X