Я бы очень опасался любого из этих ответов, который включает вычитание с плавающей запятой (например, fabs (a-b) & lt; epsilon). Во-первых, числа с плавающей запятой становятся более разреженными при больших величинах и при достаточно больших величинах, где интервал больше, чем epsilon, вы можете просто делать == b. Во-вторых, вычитая два очень близких числа с плавающей запятой (как они будут иметь тенденцию, учитывая, что вы ищете близкое равенство), именно то, как вы получаете катастрофическую отмену .
Пока не переносимый, я думаю, что ответ Гром делает все возможное, чтобы избежать этих проблем.
Я не думаю, что есть лучший человек, чтобы ответить на это, чем Эрик Липперт (выделение в оригинале):
В C # "volatile" означает не только «убедитесь, что компилятор и джиттер не выполняют каких-либо переупорядочений кода или оптимизируют кэширование регистров для этой переменной». Это также означает, что «попросите процессоры сделать то, что им нужно, чтобы убедиться, что я читаю последнее значение, даже если это означает остановить другие процессоры и заставить их синхронизировать основную память с их кешами».
Собственно, последний бит - ложь. Истинная семантика неустойчивых чтений и записей значительно сложнее, чем я здесь изложил; на самом деле они фактически не гарантируют, что каждый процессор останавливает то, что он делает, и обновляет кеши в / из основной памяти. Скорее, они обеспечивают более слабые гарантии того, как доступ к памяти до и после чтения и записи может наблюдаться для упорядочения друг относительно друга. Некоторые операции, такие как создание нового потока, ввод блокировки или использование одного из методов семейства блокировок, обеспечивают более надежные гарантии наблюдения за порядком. Если вы хотите получить более подробную информацию, прочитайте разделы 3.10 и 10.5.3 спецификации C # 4.0.
Честно говоря, я препятствую вам когда-либо создавать поле volatile. Неустойчивые поля являются признаком того, что вы делаете что-то совершенно безумное: вы пытаетесь читать и писать одно и то же значение на двух разных потоках, не помещая блокировку на место. Замки гарантируют, что память, считываемая или измененная внутри замка, будет последовательной, блокировки гарантируют, что только один поток обращается к заданному блоку памяти за раз и так далее. Количество ситуаций, в которых блокировка слишком медленная, очень мала, и вероятность того, что вы собираетесь получить код неправильно, потому что вы не понимаете, что точная модель памяти очень велика. Я не пытаюсь написать код с низким уровнем блокировки, за исключением самых простых операций блокировки. Я оставляю использование «изменчивых» для реальных экспертов.
blockquote>Для дальнейшего чтения см .:
Если вы используете .NET 1.1, ключевое слово volatile необходимо при двойном проверке блокировки. Зачем? Поскольку до .NET 2.0 следующий сценарий мог привести к тому, что второй поток получил доступ к непунктовому, но еще не полностью сконструированному объекту:
До .NET 2.0 this.foo может быть назначен новый экземпляр Foo, прежде чем конструктор будет запущен. В этом случае может появиться второй поток (во время вызова потока 1 к конструктору Foo) и испытать следующее:
До .NET 2.0 вы можете объявить this.foo нестабильным, чтобы обойти эту проблему. Начиная с .NET 2.0 вам больше не нужно использовать ключевое слово volatile для выполнения двойной блокировки.
В Википедии есть хорошая статья о Double Checked Locking и кратко затрагивает эту тему: http: //en.wikipedia.org/wiki/Double-checked_locking
Иногда компилятор оптимизирует поле и использует регистр для его хранения. Если поток 1 выполняет запись в поле, а другой поток обращается к нему, поскольку обновление хранилось в регистре (а не в памяти), второй поток получал бы устаревшие данные.
Вы можете думать об изменчивости ключевое слово, говорящее компилятору «Я хочу, чтобы вы сохранили это значение в памяти». Это гарантирует, что второй поток получит последнее значение.
Из MSDN : изменчивый модификатор обычно используется для поля, к которому обращаются несколько потоков, без использования оператора блокировки для сериализации доступа. Использование изменчивого модификатора гарантирует, что один поток извлекает самое современное значение, написанное другим потоком.
CLR любит оптимизировать инструкции, поэтому при доступе к полю в коде он может не всегда получать доступ к текущему значению поля (это может быть из стека и т. д.). Маркировка поля как volatile
гарантирует, что текущее значение поля будет доступно инструкцией. Это полезно, когда значение может быть изменено (в сценарии без блокировки) параллельным потоком в вашей программе или другим кодом, запущенным в операционной системе.
Вы, очевидно, теряете некоторую оптимизацию, но это делает сохраните код более простым.
Если вы хотите получить немного больше информации о том, что такое ключевое слово 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) для правильной обработки данных.
несколько потоков могут обращаться к переменной. Последнее обновление будет на переменной
volatile
, будут там в силу блокировки – Ohad Schneider 15 May 2015 в 00:16