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

Почему volatile и MemoryBarrier не препятствуют переупорядочению операций?

Если я понимаю значение volatile и MemoryBarrier правильно, чем нижеприведенная программа никогда не сможет показать какой-либо результат.

Он ловит переупорядочение операций записи каждый раз, когда я его запускаю. Не имеет значения, запускаю ли я его в Debug или Release. Это также не имеет значения, если я запускаю его как 32-битное или 64-битное приложение.

Почему это происходит?

    using System;
    using System.Threading;
    using System.Threading.Tasks;

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables as volatile should instruct compiler to 
            //flush all caches from registers into the memory.
            static volatile int a;
            static volatile int 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++;
                    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)
                {
                    //Using memory barrier should prevent opration reordering.
                    a = 1;
                    Thread.MemoryBarrier();
                    b = 10;
                    Thread.MemoryBarrier();
                    b = 20;
                    Thread.MemoryBarrier();
                    a = 2;

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

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

Ответ 1

Я не думаю, что это переупорядочение.

Этот фрагмент кода просто не является потокобезопасным:

 while (continueChecking)
 {
     int tempA = a;
     int tempB = b;
     ...

Я думаю, что этот сценарий возможен:

  • int tempA = a; выполняется со значениями последнего цикла (a == 2)
  • Существует контекстный переключатель в поток записи
  • b = 10, и цикл останавливается
  • Существует контекстный переключатель в поток проверки
  • int tempB = b; выполняется с помощью b == 10

Я замечаю, что вызовы MemoryBarrier() повышают шансы на этот сценарий. Вероятно, потому что они вызывают большее переключение контекста.

Ответ 2

Вы не очищаете переменные между тестами, поэтому (для всех, кроме первого) изначально a есть 2, а b - 20 - до Write имеет сделал что-нибудь.

Check может получить это начальное значение a (так tempA есть 2), а затем Write может войти, дойти до изменения b до 10.

Теперь Check читает b (поэтому tempB is 10).

Et voila. Не требуется повторного заказа для воспроизведения.

Reset a и b до 0 между прогонами, и я ожидаю, что оно исчезнет.

изменить: подтверждено; "как есть" Я получаю проблему почти сразу (< 2000 итераций); но добавив:

while (continueTrying)
{
    a = b = 0; // reset <======= added this

он затем петли на любое количество времени без каких-либо проблем.

Или как поток:

Write                   A=  B=        Check

(except first run)      2   20
                                      int tempA = a;
a = 1;                  1   20
Thread.MemoryBarrier();
b = 10;                 1   10
                                      int tempB = b;

Ответ 3

Результат не имеет ничего общего с переупорядочением, с памятью или с изменчивым. Все эти конструкции необходимы, чтобы избежать эффектов компиляции или переупорядочения процессором инструкций.

Но эта программа даст тот же результат, даже если предположить полностью согласованную модель памяти с одним процессором и оптимизацию компилятора.

Прежде всего, обратите внимание, что параллельно будут запущены несколько задач Write(). Они выполняются последовательно из-за lock() внутри Write(), но метод signle Check() может читать a и b, созданные разными экземплярами задач Write().

Поскольку функция Check() не имеет синхронизации с функцией Write - она ​​может читать a и b в два произвольных и разных момента. В коде нет ничего, что предотвращало бы Check() от чтения a, созданного предыдущим Write(), в один момент, а затем чтение b, созданное следующим образом Write() в другой момент. Прежде всего вам нужна синхронизация (блокировка) в Check(), а затем вы можете (но, вероятно, не в этом случае) нуждаться в барьерах памяти и нестабильны для борьбы с проблемами модели памяти.

Это все, что вам нужно:

        int tempA, tempB;
        lock (locker)
        {
            tempA = a;
            tempB = b;
        }

Ответ 4

  • Если вы используете MemoryBarrier в writer, почему бы вам не сделать это в checker? Поместите Thread.MemoryBarrier(); до int tempA = a;.

  • Вызов Thread.MemoryBarrier(); так много раз блокирует все преимущества метода. Вызовите его только один раз до или после a = 1;.