Почему C# volatile не защищает перезапись чтения-записи?
Согласно этой онлайн-книге, volatile
Ключевое слово в C# не защищает от переупорядочения операций записи, за которыми следуют операции чтения. Это дает этот пример, в котором оба 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:
Чтение изменчивого поля называется изменчивым чтением. Изменчивое чтение имеет "приобретенную семантику"; то есть гарантированно произойдет до любых ссылок на память, которые происходят после нее в последовательности команд.
Запись изменяемого поля называется изменяемой записью. Изменчивая запись имеет "семантику релиза"; то есть это гарантированно произойдет после любых ссылок на память перед инструкцией записи в последовательности команд.
Что является причиной этого? Есть ли случай использования, в котором мы не возражаем против переупорядочения операций записи-чтения?
3 ответа
Volatile не гарантирует, что операции чтения и записи не будут переупорядочены, а только гарантирует, что операции чтения получат самое последнее значение (без кэширования).
http://msdn.microsoft.com/en-us/library/x13ttww7%28v=vs.71%29.aspx
Система всегда считывает текущее значение энергозависимого объекта в точке, в которой оно запрашивается, даже если предыдущая инструкция запрашивала значение из того же объекта. Также значение объекта пишется сразу при назначении.
Модификатор volatile обычно используется для поля, к которому обращаются несколько потоков, без использования оператора блокировки для сериализации доступа. Использование модификатора volatile гарантирует, что один поток получит наиболее актуальное значение, записанное другим потоком.
Всякий раз, когда у вас есть несколько зависимых операций, вам нужно использовать какой-то другой механизм синхронизации. Обычно используют lock
это проще всего и создает узкие места в производительности только при злоупотреблении или в очень экстремальных ситуациях.
Предотвращение volatile write
с volatile read
переупорядочение будет довольно дорогим для архитектуры x86/x64. Это из-за оптимизации записи под названием store buffers
, Java пошла по этому пути, и изменчивые записи в Java являются фактически полными барьерами памяти на уровне инструкций процессора.
Может быть, слишком поздно, чтобы ответить... но я огляделся и увидел это. Volatile-read - это вызов метода, подобного следующему:
public static int VolatileRead(ref int address)
{
int num = address;
Thread.MemoryBarrier();
return num;
}
А Volatile-напиши вот так:
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;