it-swarm-ru.tech

Когда ключевое слово volatile должно использоваться в C #?

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

279
Doron Yaacoby

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

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

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

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

Для дальнейшего чтения смотрите:

254
Ohad Schneider

Если вы хотите немного больше узнать о том, что делает ключевое слово 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. Для энергонезависимого цикла (первого) компилятор назначил i в регистр eax. Довольно просто. Однако есть пара интересных битов - инструкция lea ebx, [ebx] фактически является многобайтовой nop-инструкцией, поэтому цикл переходит к 16-байтовому выровненному адресу памяти. Другое - использование edx для увеличения счетчика цикла вместо использования инструкции inc eax. Команда add reg, reg имеет меньшую задержку на нескольких ядрах IA32 по сравнению с инструкцией inc reg, но никогда не имеет более высокой задержки. 

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

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

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

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

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

54
Skizz

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

  1. Поток 1 спрашивает, является ли переменная нулевой . // if (this.foo == null)
  2. Поток 1 определяет, что переменная равна нулю, поэтому вводит блокировку . // блокировка (this.bar)
  3. Поток 1 спрашивает СНОВА, является ли переменная нулевой . // if (this.foo == null)
  4. Поток 1 по-прежнему определяет, что переменная имеет значение null, поэтому он вызывает конструктор и присваивает значение переменной . // this.foo = new Foo ();

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

  1. Поток 2 спрашивает, является ли переменная нулевой. // if (this.foo == null)
  2. Поток 2 определяет, что переменная НЕ является нулевой, поэтому пытается использовать ее . // this.foo.MakeFoo ()

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

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

38
AndrewTek

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

22
Dr. Bob

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

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

20
Benoit

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

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

13
Joseph Daigle

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

 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, вы не ожидаете вывода или «Value: 10» в качестве результата. Возможно, компилятор переключает строку внутри функции t1. Если затем выполняется t2, это может означать, что _flag имеет значение 5, а _value имеет 0. Таким образом, ожидаемая логика может быть нарушена. 

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

private static volatile int _flag = 0;

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

0
Aliaksei Maniuk

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

0
Paul Easter