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

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

<p>я не думаю, что есть лучший человек, чтобы ответить на это, чем <a href="http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx">эрик липперт</a> (выделение в оригинале): </p><blockquote> <p>в c # «volatile» означает не только «убедитесь, что компилятор и jitter не выполняют переупорядочение кода или не регистрируют кэширование оптимизации для этой переменной». это также означает, что «сказать процессорам делать все, что им нужно, чтобы убедиться, что я читаю последнее значение , даже если это означает остановку других процессоров и выполнение синхронизации их основной памяти с их кэша».</p> <p>на самом деле, этот последний бит является ложью. истинная семантика изменчивых чтений и записи значительно сложнее, чем я описал здесь; в факт <strong>они на самом деле не гарантируют, что каждый процессор останавливает то, что он делает</strong> и обновляет кэши в/из основной памяти. скорее, <strong>они предоставляют более слабые гарантии о том, как доступ к памяти до и после чтения и можно наблюдать, что записи упорядочены по отношению друг к другу</strong> . некоторые операции, такие как создание нового потока, ввод блокировки или использование одного из методов семейства interlocked представит более сильный гарантии о соблюдении порядка. если вы хотите больше деталей, прочтите разделы 3.10 и 10.5.3 спецификации c # 4.0.</p> <p>честно говоря, <strong>я отговариваю вас от создания нестабильного поля</strong>. летучие поля являются признаком того, что вы делаете что-то совершенно безумное: вы попытка прочитать и записать одно и то же значение в двух разных потоках без установки замка на место. блокировки гарантируют, что память прочитана или модифицированный внутри замка соблюден, замки гарантированы что только один поток обращается к данному куску памяти одновременно, и так на. количество ситуаций, в которых блокировка слишком медленная, очень маленький, и вероятность того, что вы поймете код неправильно потому что вы не понимаете, точная модель памяти очень велика. я не пытайтесь писать какой-либо код с низкой блокировкой, кроме самого тривиального использование блокированных операций. я оставляю использование "volatile" для настоящие эксперты.</p> </blockquote><p>для дальнейшего чтения смотрите:</p><ul> <li> <a href="http://msdn.microsoft.com/en-us/magazine/cc163715.aspx">понимать влияние технологий низкого уровня блокировки в многопоточных приложениях</a> </li> <li> <a href="http://joeduffyblog.com/2010/12/04/sayonara-volatile/">сайонара летучая</a> </li> </ul>

<p>если вы хотите немного больше узнать о том, что делает ключевое слово volatile, рассмотрите следующую программу (я использую devstudio 2005):</p><pre><code>#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; } </code></pre><p>используя стандартные оптимизированные (релизные) параметры компилятора, компилятор создает следующий ассемблер (ia32):</p><pre><code>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 </code></pre><p>глядя на вывод, компилятор решил использовать регистр ecx для хранения значения переменной j. для энергонезависимого цикла (первого) компилятор назначил i в регистр eax. довольно просто. однако есть пара интересных битов - инструкция lea ebx, [ebx] фактически является многобайтовой nop-инструкцией, поэтому цикл переходит к 16-байтовому выровненному адресу памяти. другое - использование edx для увеличения счетчика цикла вместо использования инструкции inc eax. команда add reg, reg имеет меньшую задержку на нескольких ядрах ia32 по сравнению с инструкцией inc reg, но никогда не имеет более высокой задержки. </p><p>теперь для цикла со счетчиком изменчивых циклов. счетчик хранится в [esp], а ключевое слово volatile сообщает компилятору, что значение всегда должно считываться/записываться в память и никогда не присваиваться регистру. компилятор даже заходит так далеко, что не выполняет загрузку/приращение/сохранение как три различных шага (загрузка eax, inc eax, save eax) при обновлении значения счетчика, вместо этого память напрямую изменяется в одной инструкции (добавление памяти). , р). способ создания кода гарантирует, что значение счетчика цикла всегда актуально в контексте одного ядра цп. никакая операция с данными не может привести к повреждению или потере данных (следовательно, не используется load/inc/store, поскольку значение может измениться во время inc, таким образом, теряясь в хранилище). так как прерывания могут обслуживаться только после завершения текущей инструкции, данные никогда не могут быть повреждены, даже с невыровненной памятью.</p><p>после того, как вы введете в систему второй цп, ключевое слово volatile не защитит данные, которые одновременно обновляются другим цп. в приведенном выше примере вам понадобится выровнять данные, чтобы получить потенциальное повреждение. ключевое слово volatile не предотвратит потенциальное повреждение, если данные не могут быть обработаны атомарно, например, если счетчик цикла имел тип long long (64 бита), то для обновления значения потребовались бы две 32-битные операции, в середине какое прерывание может произойти и изменить данные.</p><p>таким образом, ключевое слово volatile подходит только для выровненных данных, размер которых меньше или равен размеру собственных регистров, так что операции всегда являются атомарными.</p><p>ключевое слово volatile было задумано для использования с операциями io, в которых io будет постоянно меняться, но иметь постоянный адрес, например устройство с отображением в памяти uart, а компилятор не должен продолжайте использовать первое значение, прочитанное с адреса.</p><p>если вы обрабатываете большие данные или имеете несколько процессоров, то вам потребуется система блокировки более высокого уровня (os) для правильной обработки доступа к данным.</p>

<p>если вы используете .net 1.1, ключевое слово volatile необходимо при двойной проверке блокировки. зачем? поскольку до .net 2.0 следующий сценарий мог заставить второй поток обращаться к ненулевому, но не полностью сконструированному объекту:</p><ol> <li>поток 1 спрашивает, является ли переменная нулевой . // if (this.foo == null)</li> <li>поток 1 определяет, что переменная равна нулю, поэтому вводит блокировку . // блокировка (this.bar)</li> <li>поток 1 спрашивает снова, является ли переменная нулевой . // if (this.foo == null)</li> <li>поток 1 по-прежнему определяет, что переменная имеет значение null, поэтому он вызывает конструктор и присваивает значение переменной . // this.foo = new foo ();</li> </ol><p>до версии .net 2.0 this.foo мог быть назначен новый экземпляр foo до завершения работы конструктора. в этом случае может появиться второй поток (во время вызова потока 1 для конструктора foo) и испытать следующее:</p><ol> <li>поток 2 спрашивает, является ли переменная нулевой. // if (this.foo == null)</li> <li>поток 2 определяет, что переменная не является нулевой, поэтому пытается использовать ее . // this.foo.makefoo ()</li> </ol><p>до версии .net 2.0 вы могли объявить this.foo нестабильным, чтобы обойти эту проблему. начиная с .net 2.0 вам больше не нужно использовать ключевое слово volatile для двойной блокировки.</p><p>в википедии на самом деле есть хорошая статья о блокировке с двойной проверкой, и она кратко затрагивает эту тему: <a href="http://en.wikipedia.org/wiki/double-checked\_locking">http://en.wikipedia.org/wiki/double-checked\_locking</a> </p>

<p>from <a href="http://msdn.microsoft.com/en-us/library/x13ttww7(vs.71).aspx">msdn</a> : модификатор volatile обычно используется для поля, к которому обращаются несколько потоков, без использования оператора блокировки для сериализации доступа. использование модификатора volatile гарантирует, что один поток получит самое последнее значение, записанное другим потоком.</p>

<p>иногда компилятор оптимизирует поле и использует регистр для его хранения. если поток 1 выполняет запись в поле и другой поток обращается к нему, так как обновление было сохранено в регистре (а не в памяти), 2-й поток получит устаревшие данные.</p><p>вы можете подумать о ключевом слове volatile, которое говорит компилятору: «я хочу, чтобы вы сохранили это значение в памяти». это гарантирует, что 2-й поток извлекает последнее значение.</p>

<p>clr любит оптимизировать инструкции, поэтому при доступе к полю в коде он не всегда может получить доступ к текущему значению поля (оно может быть из стека и т.д.). пометка поля как <code>volatile</code> гарантирует, что текущее значение поля будет доступно инструкции. это полезно, когда значение может быть изменено (в неблокирующем сценарии) параллельным потоком в вашей программе или другим кодом, работающим в операционной системе.</p><p>вы, очевидно, теряете некоторую оптимизацию, но она делает код более простым.</p>

<p>компилятор иногда меняет порядок операторов в коде, чтобы оптимизировать его. обычно это не проблема в однопоточной среде, но это может быть проблемой в многопоточной среде. смотрите следующий пример: </p><pre><code> 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); } }); </code></pre><p>если вы запустите t1 и t2, вы не ожидаете вывода или «value: 10» в качестве результата. возможно, компилятор переключает строку внутри функции t1. если затем выполняется t2, это может означать, что \_flag имеет значение 5, а \_value имеет 0. таким образом, ожидаемая логика может быть нарушена. </p><p>чтобы исправить это, вы можете использовать<strong> volatile </strong>ключевое слово, которое вы можете применить к полю. этот оператор отключает оптимизацию компилятора, поэтому вы можете установить правильный порядок в вашем коде.</p><pre><code>private static volatile int \_flag = 0; </code></pre><p>вы должны использовать<strong> volatile </strong>только если вам это действительно нужно, потому что это отключает определенные оптимизации компилятора, это ухудшит производительность. он также не поддерживается всеми языками .net (visual basic не поддерживает его), поэтому он препятствует взаимодействию языков.</p>

<p>итак, чтобы подвести итог всего этого, правильный ответ на вопрос: если ваш код выполняется во время выполнения 2.0 или более поздней, ключевое слово volatile почти никогда не требуется и приносит больше вреда, чем пользы, если используется без необходимости. и.е. никогда не используйте это. но в более ранних версиях среды выполнения это is требовалось для правильной двойной проверки блокировки на статических полях. в частности, статические поля, класс которых имеет код инициализации статического класса.</p>