Когда следует использовать ключевое слово volatile в С#?

Может ли кто-нибудь дать хорошее объяснение ключевому слову volatile в С#? Какие проблемы он решает, а какие нет? В каких случаях это избавит меня от использования блокировки?

Ответ 1

Я не думаю, что лучше ответить на этот вопрос, чем Эрик Липперт (акцент в оригинале):

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

Собственно, этот последний бит - ложь. Истинная семантика неустойчивых чтений и записи значительно сложнее, чем я описал здесь; в факт , они фактически не гарантируют, что каждый процессор останавливает то, что он делает и обновляет кеши в/из основной памяти. Скорее, они предоставляют более слабые гарантии того, как доступ к памяти до и после чтения и могут наблюдаться упорядочивания друг относительно друга. Некоторые операции, такие как создание нового потока, ввод блокировки или использование одного из семейств методов Interlocked гарантии соблюдения порядка заказа. Если вы хотите получить более подробную информацию, прочитайте разделы 3.10 и 10.5.3 спецификации С# 4.0.

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

Подробнее читайте:

Ответ 2

Если вы хотите получить немного больше информации о том, что такое ключевое слово volatile, рассмотрите следующую программу (я использую DevStudio 2005):

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

Используя стандартные оптимизированные (выпускные) параметры компилятора, компилятор создает следующий ассемблер (IA32):

void main()
{
00401000  push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

Глядя на результат, компилятор решил использовать регистр ecx для хранения значения переменной j. Для нелетучего цикла (первый) компилятор назначил я регистру eax. Довольно просто. Есть пара интересных бит, хотя - команда lea ebx, [ebx] - это команда многобайтового nop, так что цикл перескакивает на 16-байтовый адрес выровненной памяти. Другим является использование edx для увеличения счетчика циклов вместо использования команды inc eax. Команда add reg, reg имеет более низкую задержку на нескольких ядрах IA32 по сравнению с инструкцией inc reg, но никогда не имеет более высокой задержки.

Теперь для цикла с счетчиком волатильных циклов. Счетчик хранится в [esp], а ключевое слово volatile сообщает компилятору, что значение всегда должно считываться из/записываться в память и никогда не присваиваться регистру. Компилятор даже доходит до того, что при обновлении значения счетчика не выполняется загрузка/приращение/сохранение в виде трех различных шагов (load eax, inc eax, save eax), вместо этого память непосредственно изменяется в одной команде (добавление mem, р). Способ создания кода гарантирует, что значение счетчика циклов всегда актуально в контексте одного ядра процессора. Никакая операция с данными не может привести к повреждению или потере данных (следовательно, не использовать загрузку/инк/хранилище, поскольку значение может меняться во время inc, таким образом, теряется в магазине). Поскольку прерывания могут обслуживаться только после завершения текущей команды, данные никогда не могут быть повреждены, даже при неизмененной памяти.

Как только вы вводите второй процессор в систему, ключевое слово volatile не будет защищать данные, обновляемые другим процессором одновременно. В приведенном выше примере вам нужно, чтобы данные были неровными, чтобы получить потенциальную коррупцию. Ключевое слово volatile не предотвратит потенциальное повреждение, если данные не могут обрабатываться атомарно, например, если счетчик циклов имел тип long long (64 бит), тогда для обновления значения потребуется две 32-битные операции, в середине который может прерывать и изменить данные.

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

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

Если вы работаете с большими данными или имеете несколько процессоров, вам понадобится система блокировки более высокого уровня (OS) для правильного доступа к данным.

Ответ 3

Если вы используете .NET 1.1, ключевое слово volatile необходимо при двойном проверке блокировки. Зачем? Поскольку до .NET 2.0 следующий сценарий мог заставить второй поток получить доступ к непунктовому, но еще не полностью сконструированному объекту:

  • В потоке 1 задается вопрос, является ли переменная нулевой. //if (this.foo == null)
  • Тема 1 определяет, что переменная имеет значение null, поэтому вводит блокировку. //lock (this.bar)
  • Тема 1 запрашивает AGAIN, если переменная имеет значение null. //if (this.foo == null)
  • Thread 1 все еще определяет, что переменная имеет значение null, поэтому она вызывает конструктор и присваивает значение переменной. //this.foo = new Foo();

До .NET 2.0 this.foo может быть назначен новый экземпляр Foo, прежде чем конструктор будет запущен. В этом случае второй поток может войти (во время вызова потока 1 к конструктору Foo) и испытать следующее:

  • В Thread 2 спрашивается, равна ли переменная. //if (this.foo == null)
  • В Thread 2 определяется, что переменная не равна null, поэтому пытается ее использовать. //this.foo.MakeFoo()

До .NET 2.0 вы можете объявить this.foo нестабильным, чтобы обойти эту проблему. Начиная с .NET 2.0 вам больше не нужно использовать ключевое слово volatile для выполнения двойной блокировки.

В Википедии есть хорошая статья о Double Checked Locking и кратко затрагивает эту тему: http://en.wikipedia.org/wiki/Double-checked_locking

Ответ 4

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

Ответ 5

Иногда компилятор оптимизирует поле и использует регистр для его хранения. Если поток 1 выполняет запись в поле, а другой поток обращается к нему, поскольку обновление хранилось в регистре (а не в памяти), второй поток получал устаревшие данные.

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

Ответ 6

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

Вы, очевидно, теряете некоторую оптимизацию, но это делает код более простым.

Ответ 7

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

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

Если вы запустили t1 и t2, вы не ожидали вывода или "Значение: 10" в качестве результата. Может быть, компилятор переключает линию внутри функции t1. Если t2 выполняется, возможно, что _flag имеет значение 5, но _value имеет 0. Таким образом, ожидаемая логика может быть нарушена.

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

private static volatile int _flag = 0;

Вы должны использовать volatile только в том случае, если он вам действительно нужен, поскольку он отключает определенные оптимизации компилятора, что ухудшит производительность. Он также не поддерживается всеми .NET-языками (Visual Basic не поддерживает его), поэтому он препятствует взаимодействию языков.

Ответ 8

Итак, чтобы подвести итог всего этого, правильный ответ на вопрос таков: если ваш код выполняется во время выполнения 2.0 или более поздней, ключевое слово volatile почти никогда не требуется и приносит больше вреда, чем пользы, если используется без необходимости. IE Никогда не используйте его. НО в более ранних версиях среды выполнения это необходимо для правильной двойной проверки блокировки статических полей. В частности, статические поля, класс которых имеет код инициализации статического класса.

Ответ 9

несколько потоков могут получить доступ к переменной. Последнее обновление будет на переменной