Есть ли смысл использовать volatile в .NET? [Дубликат]

Я бы очень опасался любого из этих ответов, который включает вычитание с плавающей запятой (например, fabs (a-b) & lt; epsilon). Во-первых, числа с плавающей запятой становятся более разреженными при больших величинах и при достаточно больших величинах, где интервал больше, чем epsilon, вы можете просто делать == b. Во-вторых, вычитая два очень близких числа с плавающей запятой (как они будут иметь тенденцию, учитывая, что вы ищете близкое равенство), именно то, как вы получаете катастрофическую отмену .

Пока не переносимый, я думаю, что ответ Гром делает все возможное, чтобы избежать этих проблем.

260
задан Doron Yaacoby 6 December 2008 в 21:04
поделиться

8 ответов

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

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

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

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

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

241
ответ дан Jason Evans 22 August 2018 в 03:56
поделиться
  • 1
    Я бы проголосовал за это, если мог. Там много интересной информации, но на самом деле он не отвечает на его вопрос. Он спрашивает об использовании ключевого слова volatile, связанного с блокировкой. В течение довольно долгого времени (до 2.0 RT) ключевое слово volatile было необходимо использовать для правильного создания потока статического поля, если экземпляр поля имел какой-либо код инициализации в конструкторе (см. Ответ AndrewTek). В производственных средах много кода RT RT, а разработчики, которые его поддерживают, должны знать, почему это ключевое слово существует, и если его безопасно удалить. – Paul Easter 1 January 2015 в 23:43
  • 2
    @PaulEaster тот факт, что он может использоваться для блокировки doulbe-check (обычно в шаблоне singleton), не означает, что он должен . Опираясь на модель памяти .NET, вероятно, является плохой практикой - вместо этого вы должны полагаться на модель ECMA. Например, вы можете захотеть портировать моно в один день, что может иметь другую модель. Я также должен понимать, что различные аппаратные архитектуры могут изменить ситуацию. Для получения дополнительной информации см. stackoverflow.com/a/7230679/67824 . Для лучших одноэлементных альтернатив (для всех версий .NET) см.: csharpindepth.com/articles/general/singleton.aspx – Ohad Schneider 2 January 2015 в 11:20
  • 3
    Другими словами, правильный ответ на вопрос: если ваш код работает в среде выполнения 2.0 или более поздней версии, ключевое слово volatile почти никогда не требуется и приносит больше вреда, чем пользы, если его использовать без необходимости. Но в более ранних версиях среды выполнения она необходима для правильной блокировки двойной проверки в статических полях. – Paul Easter 3 January 2015 в 03:32
  • 4
    @PaulEaster Да, но я утверждаю, что дважды проверенная блокировка не нужна и может быть заменена безопасной альтернативой статьи I, связанной с (в частности, решением 5). – Ohad Schneider 3 January 2015 в 12:03
  • 5
    @Giorgi да - барьеры памяти, гарантированные volatile, будут там в силу блокировки – Ohad Schneider 15 May 2015 в 00:16
1
ответ дан Aliaksei Maniuk 22 August 2018 в 03:56
поделиться

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

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

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

  1. В Thread 2 спрашивается, равна ли переменная null. //if(this.foo == null)
  2. В 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

34
ответ дан AndrewTek 22 August 2018 в 03:56
поделиться

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

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

20
ответ дан Benoit 22 August 2018 в 03:56
поделиться

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

22
ответ дан Dr. Bob 22 August 2018 в 03:56
поделиться

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

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

13
ответ дан Joseph Daigle 22 August 2018 в 03:56
поделиться

Если вы хотите получить немного больше информации о том, что такое ключевое слово 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 сообщает компилятору, что значение всегда должно считываться из / записываться в память и никогда не присваиваться регистру. Компилятор даже доходит до того, что при обновлении значения счетчика не выполняется загрузка / приращение / сохранение в виде трех различных шагов (load eax, inc eax, save eax), вместо этого память непосредственно изменяется в одной команде (добавление mem , р). Способ создания кода гарантирует, что значение счетчика циклов всегда актуально в контексте одного ядра процессора. Никакая операция с данными не может привести к повреждению или потере данных (следовательно, не использовать загрузку / инк / хранилище, поскольку значение может меняться во время inc, таким образом, теряется в магазине). Поскольку прерывания могут обслуживаться только после завершения текущей команды, данные никогда не могут быть повреждены, даже с невыровненной памятью.

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

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

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

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

54
ответ дан Tim Cooper 22 August 2018 в 03:56
поделиться
  • 1
    Это C ++, но этот принцип применяется к C #. – Skizz 16 September 2008 в 16:09
  • 2
  • 3
    Эрик Липперт пишет, что волатильность на C ++ препятствует компилятору выполнять некоторые оптимизации, в то время как в C # volatile дополнительно происходит некоторая связь между другими ядрами / процессорами, чтобы обеспечить считывание последнего значения. – Peter Huber 26 January 2017 в 08:00

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

-2
ответ дан user2943601 22 August 2018 в 03:56
поделиться
Другие вопросы по тегам:

Похожие вопросы: