Почему С# volatile защищает переупорядочение записи?

В соответствии с этой онлайн-книгой ключевое слово volatile в С# не защищает от переупорядочения операций записи, за которыми следуют операции чтения. Он дает этот пример, в котором оба a и b могут быть установлены на 0, несмотря на то, что x и y являются volatile:

class IfYouThinkYouUnderstandVolatile
{
  volatile int x, y;

  void Test1()        // Executed on one thread
  {
    x = 1;            // Volatile write (release-fence)
    int a = y;        // Volatile read (acquire-fence)
    ...
  }

  void Test2()        // Executed on another thread
  {
    y = 1;            // Volatile write (release-fence)
    int b = x;        // Volatile read (acquire-fence)
    ...
  }
}

Это похоже на то, что в спецификации указано в 10.5.3:

Чтение изменчивого поля называется изменчивым чтением. Неустойчивое чтение имеет "приобретать семантику"; то есть он гарантированно будет происходить до любых ссылок на память, которые происходят после него в последовательности команд.

Запись изменчивого поля называется volatile write. У изменчивой записи есть "семантика выпуска"; то есть это гарантировано произойдет после любых ссылок на память до команды записи в последовательности команд.

В чем причина этого? Есть ли прецедент, в котором мы не против перезаписывания операций Write-Read?

Ответы

Ответ 1

Volatile не гарантирует, что чтения и записи не будут перенаправлены, это только гарантирует, что чтение получает самое последнее значение (не кэшированное).

http://msdn.microsoft.com/en-us/library/x13ttww7%28v=vs.71%29.aspx

Система всегда считывает текущее значение изменчивого объекта в запрошенной точке, даже если предыдущая команда запрашивала значение от одного и того же объекта. Кроме того, значение объекта записывается сразу при назначении.

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

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

Ответ 2

Может быть, слишком поздно, чтобы ответить... но я оглядывался и видел это. Volatile-read - это вызов метода, аналогичного следующему:

public static int VolatileRead(ref int address)
{
    int num = address;
    Thread.MemoryBarrier();
    return num;
}

И volatile-write выглядит так:

public static int VolatileWrite(ref int address, int value)
{
    Thread.MemoryBarrier();
    adrdress = value;
}

Инструкция MemoryBarrier(); - это та, которая предотвращает переупорядочение. MemoryBarrier(); гарантирует, что инструкции перед выполнением перед выполнением инструкций после. Когда VW затем VR, вы будете иметь:

Thread.MemoryBarrier();
adrdress = value; //this line may be reordered with the one bellow
int num = address;//this line may be reordered with the one above
Thread.MemoryBarrier();
return num;

Ответ 3

Предотвращение volatile write с переупорядочением volatile read будет довольно дорогостоящим в архитектуре x86/x64. Это из-за оптимизации записи называется store buffers. Java пошла по этому пути, и волатильная запись в Java фактически полностью перекрывает память на уровне инструкций процессора.