Иллюстрирование использования ключевого слова volatile в C # - PullRequest
87 голосов
/ 25 сентября 2008

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

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

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

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

Ответы [ 6 ]

101 голосов
/ 16 августа 2009

Я добился рабочего примера!

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

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

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");
    }
}
21 голосов
/ 25 сентября 2008

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

6 голосов
/ 25 сентября 2008

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

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

Это поведение не то же самое, что и в C ++.

MSDN имеет краткое описание здесь . Вот, возможно, более глубокий пост на темы Волатильность, атомарность и взаимосвязь

4 голосов
/ 09 ноября 2012

Вот мой вклад в коллективное понимание этого поведения ... Это не так много, просто демонстрация (на основе демоверсии 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: /95466/illystrirovanie-ispolzovaniya-klychevogo-slova-volatile-v-c-#
      // 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 это).

Приветствия, и спасибо за всю рыбу. Кит.


PS: Я бы очень заинтересовался демонстрацией исходного запроса: «Я хотел бы видеть a static volatile int * 1017» * ведет себя правильно, где a статический int плохо себя ведет.

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

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

4 голосов
/ 25 сентября 2008

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

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

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

Другим примером является Ожидание при занятости.

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

2 голосов
/ 04 февраля 2014

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

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

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