Когда ключевое слово volatile должно использоваться в C #? - PullRequest
284 голосов
/ 16 сентября 2008

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

Ответы [ 9 ]

260 голосов
/ 08 июля 2013

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

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

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

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

Подробнее см .:

55 голосов
/ 16 сентября 2008

Если вы хотите немного больше узнать о том, что делает ключевое слово 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 было задумано для использования с операциями ввода-вывода, в которых ввод-вывод постоянно менялся, но имел постоянный адрес, например устройство UART с отображением в памяти, и компилятору не следует продолжать повторное использование первого значения, считанного с адреса. .

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

38 голосов
/ 28 февраля 2014

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

  1. Поток 1 спрашивает, является ли переменная нулевой. //if(this.foo == null)
  2. Поток 1 определяет, что переменная равна нулю, поэтому вводит блокировку. //lock(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

21 голосов
/ 16 сентября 2008

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

20 голосов
/ 16 сентября 2008

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

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

13 голосов
/ 16 сентября 2008

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

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

1 голос
/ 14 декабря 2018

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

0 голосов
/ 08 февраля 2016

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

 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 не поддерживает его), поэтому он препятствует взаимодействию языков.

0 голосов
/ 04 ноября 2013

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...