2008-09-16 5 views
250

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

+5

Почему вы хотите сэкономить на использовании блокировки? Незаконные блокировки добавляют к вашей программе несколько наносекунд. Вы действительно не можете позволить себе несколько наносекунд? – 2013-07-08 20:24:55

ответ

231

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

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

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

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

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

+19

Я бы проголосовал за это, если мог. Там много интересной информации, но на самом деле он не отвечает на его вопрос. Он спрашивает об использовании ключевого слова volatile, связанного с блокировкой. В течение довольно долгого времени (до 2.0 RT) ключевое слово volatile было необходимо использовать для правильного создания потока статического поля, если экземпляр поля имел какой-либо код инициализации в конструкторе (см. Ответ AndrewTek). В производственных средах много кода RT RT, а разработчики, которые его поддерживают, должны знать, почему это ключевое слово существует, и если его безопасно удалить. – 2015-01-01 22:43:54

13

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

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

22

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

20

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

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

54

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

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

+0

Это C++, но этот принцип применим к C#. – Skizz 2008-09-16 15:09:15

+8

Не совсем: http://www.ddj.com/hpc-high-performance-computing/212701484 – 2009-01-13 00:33:56

+4

Эрик Липперт пишет, что волатильность на C++ препятствует компилятору выполнять некоторые оптимизации, в то время как в C# volatile дополнительно происходит некоторая связь между других ядер/процессоров, чтобы обеспечить считывание последнего значения. – 2017-01-26 07:00:48

-2

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

33

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

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

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

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

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

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

1

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

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; 

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

Смежные вопросы