Иллюстрирование использования ключевого слова volatile в С#

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

Добавление ключевого слова volatile в ту же программу должно устранить проблему.

Того, чего я не смог достичь. Даже пытаясь несколько раз, обеспечивая оптимизацию и т.д., Я всегда получаю правильное поведение без ключевого слова volatile.

Есть ли у вас какие-либо идеи по этой теме? Вы знаете, как имитировать такую ​​проблему в простом демонстрационном приложении? Это зависит от оборудования?

Ответ 1

Я получил рабочий пример!

Основная идея получена из wiki, но с некоторыми изменениями для С#. Статья wiki демонстрирует это для статического поля С++, похоже, что С# всегда тщательно компилирует запросы на статические поля... и я делаю пример с нестационарным:

Если вы запустите этот пример в режиме Release и без отладчика (Ctrl + F5), тогда будет оптимизирована строка 'while (test.foo!= 255)' to 'while (true)', и эта программа никогда не возвращается. Но после добавления ключевого слова "volatile" вы всегда получаете "OK".

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}

Ответ 2

Да, это зависит от оборудования (вы вряд ли увидите проблему без нескольких процессоров), но она также зависит от реализации. Спецификации модели памяти в спецификации CLR допускают те вещи, которые реализация CLR для Microsoft не обязательно делает. Лучшая документация, которую я видел в ключевом слове volatile, - это в этом блоге Джо Даффи. Обратите внимание, что он говорит, что документация MSDN "сильно вводит в заблуждение".

Ответ 3

На самом деле это не ошибка, когда ключевое слово "volatile" не указано, более того, ошибка может произойти, если она не указана. Как правило, вы узнаете, когда это лучше, чем компилятор!

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

Такое поведение не является тем же ключевым словом, что и в С++.

В MSDN есть краткое описание здесь. Вот, возможно, более подробное сообщение по темам Волатильность, атомарность и блокировка

Ответ 4

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

В Википедии есть хороший пример того, как продемонстрировать это на C.

То же самое может произойти в С#, если компилятор JIT решит, что значение переменной не может измениться в любом случае и, таким образом, создает машинный код, который даже не проверяет его. Если теперь другой поток меняет значение, ваш первый поток все равно может быть пойман в цикле.

Еще один пример - ожидание ожидания.

Опять же, это может произойти и с С#, но это сильно зависит от виртуальной машины и от JIT-компилятора (или интерпретатора, если он не имеет JIT... в теории, я думаю, что MS всегда использует компилятор JIT и также Mono использует один, но вы можете отключить его вручную).

Ответ 5

Здесь мой вклад в коллективное понимание этого поведения... Это не так много, просто демонстрация (основанная на демонстрации xkip), которая показывает поведение изменчивых стихов как энергонезависимое (т.е. "нормальное" ) значение int, бок о бок, в той же программе... вот что я искал, когда нашел этот поток.

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

Есть некоторые действительно превосходные краткие объяснения выше, а также некоторые замечательные ссылки. Спасибо всем за то, что помогли мне окунуться в volatile (по крайней мере, чтобы знать, не полагайтесь на volatile, где мой первый инстинкт был lock it).

Привет, и спасибо за ВСЕ рыбу. Кит.


PS: Меня очень интересовало бы демонстрацию исходного запроса, который был бы следующим: "Мне бы хотелось, чтобы волатильный int статический вел себя корректно, a static int misbehaves.

Я пробовал и проваливал этот вызов. (На самом деле я сдался довольно быстро;-). Во всем, что я пробовал со статическими варами, они ведут себя "правильно" независимо от того, являются ли они неустойчивыми... и мне бы хотелось объяснить ПОЧЕМУ, что это так, если это действительно так. Разве что компилятор не кэширует значения статических варов в регистре (т.е. вместо этого кэширует ссылку на этот кучи)?

Нет, это не новый вопрос... это попытка вернуть сообщество к исходному вопросу.

Ответ 6

Я наткнулся на следующий текст Джо Альбахари, который очень помог мне.

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

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}