Подтвердить что ты не робот

Почему этот многопоточный код печатает 6 часть времени?

Я создаю два потока и передаю функцию, которая выполняет алгоритм, показанный ниже 10 000 000 раз. В основном он пишет "5" в консоли, а иногда пишет "3" или "4". Это совершенно очевидно, почему это похоже. Но здесь возникает запутанная часть: зачем она пишет "6" в консоли?

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

Вот результат: введите описание изображения здесь

4b9b3361

Ответ 1

Я думаю, что я выяснил последовательность событий, ведущих к этой проблеме:

В потоке 1 входит if (_state == 3)

Контекстный коммутатор

Ввод 2 входит if (_state == 3)
Поток 2 увеличивает состояние (state = 4)

Контекстный коммутатор

Тема 1 читает _state как 4

Контекстный коммутатор

Резьба 2 набора _state = 3
Тема 2 входит if (_state == 3)

Контекстный коммутатор

Тема 1 выполняет _state = 4 + 1

Контекстный коммутатор

Тема 2 читает _state как 5
Тема 2 исполняет _state = 5 + 1;

Ответ 2

Это типичный состояние гонки. EDIT: На самом деле, есть несколько условий гонки.

Это может произойти в любое время, когда _state равно 3, и оба потока достигают сразу за оператором if, либо одновременно путем переключения контекста в одном ядре, либо одновременно параллельно в нескольких ядрах.

Это связано с тем, что оператор ++ сначала считывает _state, а затем увеличивает его. Возможно, что после первого оператора if вы заработали достаточно времени, чтобы прочитать 5 или даже 6.

EDIT: если вы обобщили этот пример для N потоков, вы можете увидеть число до 3 + N + 1.

Это может быть правильно, когда потоки начинают работать, или когда вы только что установили _state в 3.

Чтобы избежать этого, используйте блокировку вокруг оператора if или используйте Interlocked для доступа к _state, например if (System.Threading.Interlocked.CompareAndExchange(ref _state, 3, 4) == 3) и System.Threading.Interlocked.Exchange(ref _state, 3).

Если вы хотите сохранить условие гонки, вы должны объявить _state как volatile, иначе вы рискуете, чтобы каждый поток видел _state локально без обновлений из других потоков.

В качестве альтернативы вы можете использовать System.Threading.Volatile.Read и System.Threading.Volatile.Write, если вы включите реализацию, чтобы иметь _state в качестве переменной и Tr как замыкание, которое фиксирует эту переменную, поскольку локальные переменные не могут быть (и не сможет быть) объявлен volatile. В этом случае даже инициализация должна выполняться с помощью volatile write.


EDIT: возможно, условия гонки более очевидны, если мы немного изменим код, расширив каждое прочитанное:

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

Я добавил комментарии в местах, где _state может отличаться от предполагаемых предыдущими инструкциями чтения переменных.

Здесь длинная диаграмма, которая показывает, что даже можно печатать 6 дважды подряд, один раз в каждом потоке, как изображение, которое было опубликовано оп. Помните, что потоки могут не работать синхронно, как правило, из-за упреждающего переключения контекста, кэширования или разницы в скорости ядра (из-за экономии энергии или временной турбоскоростной скорости):

Race condition prints 6


Это похоже на оригинал, но он использует класс volatile, где state теперь является переменной, захваченной закрытием. Количество и порядок волатильных доступов становятся очевидными:

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

Некоторые поточно-безопасные подходы:

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


Этот бит немного отличается, он принимает последнее известное значение _state после приращения для выполнения чего-то:

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

Обратите внимание, что примеры Interlocked.Increment/Interlocked.Decrement небезопасны, в отличие от примеров lock/Monitor и Interlocked.CompareExchange, поскольку нет надежного способа узнать, было ли приращение успешным или нет.

Один общий подход заключается в том, чтобы увеличивать, а затем следовать с try/finally, где вы уменьшаетесь в блоке finally. Однако может быть выбрано асинхронное исключение (например, ThreadAbortException)

Асинхронные исключения могут быть выброшены в неожиданные местоположения, возможно, каждая машинная инструкция: ThreadAbortException, StackOverflowException и OutOfMemoryException.

Другой подход заключается в инициализации currentState чем-то ниже 3 и условном уменьшении в блоке finally. Но опять же, между Interlocked.Increment return и currentState назначается результат, может возникнуть асинхронное исключение, поэтому currentState может все еще иметь начальное значение, даже если Interlocked.Increment преуспел.