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

Воспроизводимый пример неустойчивого использования

Я ищу воспроизводимый пример, который может продемонстрировать, как работает volatile keyword. Я ищу что-то, что работает "неправильно", без переменных, помеченных как изменчивые, и работает "правильно" с ним.

Я имею в виду пример, который продемонстрирует, что порядок операций записи/чтения во время выполнения отличается от ожидаемого, когда переменная не помечена как изменчивая и не отличается, когда переменная не помечена как энергозависимая.

Я подумал, что у меня есть пример, но потом с помощью других я понял, что это всего лишь фрагмент неправильного многопоточного кода. Почему volatile и MemoryBarrier не препятствуют переупорядочению операций?

Я также нашел ссылку, которая демонстрирует эффект volatile в оптимизаторе, но он отличается от того, что я ищу. Он демонстрирует, что запросы к переменной, помеченной как volatile, не будут оптимизированы. Как проиллюстрировать использование ключевого слова volatile в С#

Вот где я до сих пор. Этот код не показывает никаких признаков переупорядочения операции чтения/записи. Я ищу тот, который покажет.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Runtime.CompilerServices;

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables 
            static byte a;
            static byte b;

            //Track a number of iteration that it took to detect operation reordering.
            static long iterations = 0;

            static object locker = new object();

            //Indicates that operation reordering is not found yet.
            static volatile bool continueTrying = true;

            //Indicates that Check method should continue.
            static volatile bool continueChecking = true;

            static void Main(string[] args)
            {
                //Restarting test until able to catch reordering.
                while (continueTrying)
                {
                    iterations++;
                    a = 0;
                    b = 0;
                    var checker = new Task(Check);
                    var writter = new Task(Write);
                    lock (locker)
                    {
                        continueChecking = true;
                        checker.Start();

                    }
                    writter.Start();
                    checker.Wait();
                    writter.Wait();
                }
                Console.ReadKey();
            }

            static void Write()
            {
                //Writing is locked until Main will start Check() method.
                lock (locker)
                {
                    WriteInOneDirection();
                    WriteInOtherDirection();

                    //Stops spinning in the Check method.
                    continueChecking = false;
                }
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOneDirection(){
                a = 1;
                b = 10;
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOtherDirection()
            {
                b = 20;
                a = 2;
            }

            static void Check()
            {
                //Spins until finds operation reordering or stopped by Write method.
                while (continueChecking)
                {
                    int tempA = a;
                    int tempB = b;

                    if (tempB == 10 && tempA == 2)
                    {
                        continueTrying = false;
                        Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB);
                        Console.WriteLine("In " + iterations + " iterations.");
                        break;
                    }
                }
            }
        }
    }

Edit:

Как я понимаю, оптимизация, вызывающая переупорядочение, может исходить от JITer или от самого оборудования. Я могу перефразировать мой вопрос. Процессоры JITER или x86 переупорядочивают операции чтения/записи И есть ли способ продемонстрировать это на С#, если они это делают?

4b9b3361

Ответ 1

Точная семантика волатильности является детализацией реализации дрожания. Компилятор выдает команду Opcodes.Volatile IL, где когда-либо вы обращаетесь к переменной, объявленной изменчивой. Он проверяет, что тип переменной является законным, вы не можете объявлять типы значений, превышающие 4 байта, а также то, что бакс останавливается.

Спецификация языка С# определяет поведение изменчивого, цитируемого здесь Эрика Липперта. Семантика "выпуска" и "приобретать" - это то, что имеет смысл только на ядре процессора со слабой моделью памяти. Такие процессоры на рынке не преуспели, возможно, потому, что они являются огромной болью для программ. Шансы, что ваш код когда-либо будет работать на титане, ничтожны.

Что особенно плохо связано с определением спецификации языка С#, так это то, что он вообще не упоминает, что на самом деле происходит. Объявление переменной volatile не позволяет оптимизатору дрожания оптимизировать код для хранения переменной в регистре процессора. Вот почему код, связанный с Marc, висит. Это произойдет только с текущим джиттером x86, еще одним сильным намеком на то, что volatile действительно является детализацией реализации дрожания.

Плохая семантика волатильности имеет богатую историю, она исходит из языка C. У чьих генераторов кода много проблем, и это правильно. Здесь интересен отчет об этом (pdf). Это датируется 2008 годом, хорошая 30-летняя возможность исправить ситуацию. Или неправильно, это происходит вверх, когда оптимизатор кода забывает о переменной, которая является изменчивой. Неоптимизированный код никогда не сталкивается с проблемой. Примечательно, что дрожание в версии с открытым исходным кодом .NET(SSLI20) полностью игнорирует инструкцию IL. Можно также утверждать, что текущее поведение джиттера x86 является ошибкой. Я думаю, что это не так просто, чтобы вывести его в режим сбоя. Но никто не может утверждать, что это на самом деле ошибка.

Запись на стене, только когда-либо объявляйте переменную volatile, если она хранится в регистре с отображением памяти. Исходное намерение ключевого слова. Вероятность того, что вы столкнетесь с таким использованием на языке С#, должна быть кратковременно мала, такой код принадлежит драйверу устройства. И прежде всего, никогда не предполагайте, что это полезно в многопоточном сценарии.

Ответ 2

Этот пример можно использовать для демонстрации различного поведения с volatile и без него. Этот пример должен быть скомпилирован с использованием сборки Release и запущен за пределами отладчика 1. Поэкспериментируйте, добавив и удалив ключевое слово volatile на флаг stop.

Что происходит, так это то, что чтение stop в цикле while переупорядочено так, что оно происходит до цикла, если volatile опущено. Это предотвратит завершение потока даже после того, как основной поток установил флаг stop на true.

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...");

        // The Join call should return almost immediately.
        // With volatile it DOES.
        // Without volatile it does NOT.
        t.Join(); 
    }
}

Следует также отметить, что тонкие изменения этого примера могут уменьшить его вероятность воспроизводимости. Например, добавление Thread.Sleep (возможно, для имитации чередования потоков) само введет барьер памяти и, следовательно, аналогичную семантику ключевого слова volatile. Я подозреваю, что Console.WriteLine вводит неявные барьеры памяти или иным образом предотвращает использование дрожания в операции переопределения команд. Просто имейте это в виду, если вы слишком много запугаетесь с примером.


1 Я считаю, что версия рамочной версии до 2.0 не включает эту оптимизацию переупорядочения. Это означает, что вы должны иметь возможность воспроизвести это поведение с версией 2.0 и выше, но не с более ранними версиями.