Кто-нибудь может дать хорошее объяснение ключевого слова volatile в C #? Какие проблемы он решает, а какие нет? В каких случаях это спасет меня от использования блокировки?
Кто-нибудь может дать хорошее объяснение ключевого слова volatile в C #? Какие проблемы он решает, а какие нет? В каких случаях это спасет меня от использования блокировки?
Ответы:
Я не думаю, что есть лучший человек, чтобы ответить на это, чем Эрик Липперт (акцент в оригинале):
В C # «volatile» означает не только «убедитесь, что компилятор и джиттер не выполняют переупорядочение кода или не регистрируют оптимизацию кэширования для этой переменной». Это также означает «сказать процессорам делать все, что им нужно, чтобы убедиться, что я читаю последнее значение, даже если это означает остановку других процессоров и синхронизацию основной памяти с их кешами».
На самом деле, этот последний бит является ложью. Истинная семантика изменчивых операций чтения и записи значительно сложнее, чем я описал здесь; фактически они не гарантируют, что каждый процессор останавливает свою работу и обновляет кэши в / из основной памяти. Скорее, они предоставляют более слабые гарантии того, что доступ к памяти до и после чтения и записи может быть упорядочен относительно друг друга . Некоторые операции, такие как создание нового потока, ввод блокировки или использование одного из методов семейства Interlocked, предоставляют более строгие гарантии наблюдения за порядком. Если вы хотите больше подробностей, прочитайте разделы 3.10 и 10.5.3 спецификации C # 4.0.
Честно говоря, я отговариваю вас от создания нестабильного поля . Изменчивые поля являются признаком того, что вы делаете что-то совершенно безумное: вы пытаетесь прочитать и записать одно и то же значение в двух разных потоках, не устанавливая блокировку на месте. Блокировки гарантируют, что считывание или изменение памяти внутри блокировки считается согласованным, блокировки гарантируют, что только один поток одновременно обращается к данному фрагменту памяти, и так далее. Количество ситуаций, в которых блокировка слишком медленная, очень мало, и вероятность того, что вы ошибетесь в коде, потому что не понимаете точную модель памяти, очень велика. Я не пытаюсь писать какой-либо код с низкой блокировкой, за исключением самых тривиальных операций с блокировкой. Я оставляю использование «volatile» реальным специалистам.
Для дальнейшего чтения смотрите:
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 сообщает компилятору, что значение всегда должно считываться / записываться в память и никогда не присваиваться регистру. Компилятор даже заходит так далеко, что не выполняет загрузку / приращение / сохранение как три отдельных шага (загрузка eax, inc eax, save eax) при обновлении значения счетчика, вместо этого память напрямую изменяется в одной инструкции (добавление памяти). , р). Способ создания кода гарантирует, что значение счетчика цикла всегда актуально в контексте одного ядра ЦП. Никакая операция с данными не может привести к повреждению или потере данных (следовательно, не используется load / inc / store, поскольку значение может измениться во время inc, таким образом теряясь в хранилище). Поскольку прерывания могут обслуживаться только после завершения текущей инструкции,
После того, как вы введете в систему второй ЦП, ключевое слово volatile не защитит данные, обновляемые одновременно другим ЦП. В приведенном выше примере вам понадобится выровнять данные, чтобы получить потенциальное повреждение. Ключевое слово volatile не предотвратит потенциальное повреждение, если данные не могут быть обработаны атомарно, например, если счетчик цикла имел тип long long (64 бита), то для обновления значения потребовались бы две 32-битные операции, в середине какое прерывание может произойти и изменить данные.
Таким образом, ключевое слово volatile подходит только для выровненных данных, размер которых меньше или равен размеру собственных регистров, так что операции всегда являются атомарными.
Ключевое слово volatile было задумано для использования с операциями ввода-вывода, в которых ввод-вывод постоянно менялся, но имел постоянный адрес, такой как устройство UART с отображением в памяти, и компилятору не следует повторно использовать первое значение, считанное с адреса.
Если вы обрабатываете большие данные или у вас несколько процессоров, вам потребуется система блокировки более высокого уровня (OS) для правильной обработки доступа к данным.
Если вы используете .NET 1.1, ключевое слово volatile необходимо при двойной проверке блокировки. Зачем? Поскольку до .NET 2.0 следующий сценарий мог заставить второй поток обращаться к ненулевому, но не полностью сконструированному объекту:
До версии .NET 2.0 this.foo мог быть назначен новый экземпляр Foo до завершения работы конструктора. В этом случае может прийти второй поток (во время вызова потока 1 для конструктора Foo) и испытать следующее:
До версии .NET 2.0 вы могли объявить this.foo нестабильным, чтобы обойти эту проблему. Начиная с .NET 2.0 вам больше не нужно использовать ключевое слово volatile для двойной блокировки.
В Википедии на самом деле есть хорошая статья о блокировке с двойной проверкой, и она кратко затрагивает эту тему: http://en.wikipedia.org/wiki/Double-checked_locking
foo
? Разве поток 1 не блокируется this.bar
и, следовательно, только поток 1 сможет инициализировать foo в данный момент времени? Я имею в виду, что вы проверяете значение после
Иногда компилятор оптимизирует поле и использует регистр для его хранения. Если поток 1 выполняет запись в поле и другой поток обращается к нему, так как обновление было сохранено в регистре (а не в памяти), 2-й поток получит устаревшие данные.
Вы можете подумать о ключевом слове volatile, которое говорит компилятору: «Я хочу, чтобы вы сохранили это значение в памяти». Это гарантирует, что 2-й поток извлекает последнее значение.
Из MSDN : модификатор volatile обычно используется для поля, к которому обращаются несколько потоков, без использования оператора блокировки для сериализации доступа. Использование модификатора volatile гарантирует, что один поток получит самое последнее значение, записанное другим потоком.
CLR любит оптимизировать инструкции, поэтому при доступе к полю в коде он не всегда может получить доступ к текущему значению поля (оно может быть из стека и т. Д.). Пометка поля as volatile
обеспечивает доступ к текущему значению поля с помощью инструкции. Это полезно, когда значение может быть изменено (в неблокирующем сценарии) параллельным потоком в вашей программе или другим кодом, работающим в операционной системе.
Вы, очевидно, теряете некоторую оптимизацию, но она делает код более простым.
Я нашел эту статью Joydip Kanjilal очень полезной!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
Я просто оставлю это здесь для справки
Компилятор иногда меняет порядок операторов в коде, чтобы оптимизировать его. Обычно это не проблема в однопоточной среде, но это может быть проблемой в многопоточной среде. Смотрите следующий пример:
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;
Вы должны использовать volatile только в том случае, если оно действительно вам нужно, потому что оно отключает определенные оптимизации компилятора, оно снижает производительность Он также не поддерживается всеми языками .NET (Visual Basic не поддерживает его), поэтому он препятствует взаимодействию языков.
Итак, чтобы подвести итог всего этого, правильный ответ на вопрос таков: если ваш код выполняется во время выполнения 2.0 или более поздней версии, ключевое слово volatile почти никогда не требуется и приносит больше вреда, чем пользы, если используется без необходимости. IE никогда не используй это. НО в более ранних версиях среды выполнения это необходимо для правильной двойной проверки блокировки статических полей. В частности, статические поля, класс которых имеет код инициализации статического класса.
несколько потоков могут получить доступ к переменной. Последнее обновление будет в переменной