Volatile (компьютерное программирование) - Volatile (computer programming)
В компьютерное программирование, особенно в C, C ++, C #, и Ява языки программирования, то летучий ключевое слово указывает, что ценить может меняться при разных доступах, даже если не кажется, что он был изменен. Это ключевое слово предотвращает оптимизирующий компилятор от оптимизации последующих операций чтения или записи и, следовательно, неправильного повторного использования устаревшего значения или исключения записи. Изменчивые значения в первую очередь возникают при доступе к оборудованию (ввод-вывод с отображением памяти ), где чтение или запись в память используются для связи с периферийные устройства, И в заправка, где другой поток мог изменить значение.
Несмотря на то, что это общее ключевое слово, поведение летучий
существенно различается между языками программирования и легко понимается неправильно. В C и C ++ это квалификатор типа, подобно const
, и является свойством тип. Кроме того, в C и C ++ он делает нет работают в большинстве сценариев многопоточности, и такое использование не рекомендуется. В Java и C # это свойство Переменная и указывает, что объект с которой связана переменная, может изменяться и специально предназначена для многопоточности. в D язык программирования, есть отдельное ключевое слово общий
для использования потоковой передачи, но нет летучий
ключевое слово существует.
В C и C ++
В C и, следовательно, C ++ летучий
ключевое слово было предназначено[1]
- разрешить доступ к ввод-вывод с отображением памяти устройства
- разрешить использование переменных между
setjmp
иlongjmp
- разрешить использование
sig_atomic_t
переменные в обработчиках сигналов.
Операции на летучий
переменные не атомный, и при этом они не устанавливают правильную связь «происходит до» для потоковой передачи. Это указано в соответствующих стандартах (C, C ++, POSIX, WIN32),[1] а изменчивые переменные не являются потокобезопасными в подавляющем большинстве текущих реализаций. Таким образом, использование летучий
ключевое слово как переносимый механизм синхронизации не одобряется многими группами C / C ++.[2][3][4]
Пример ввода-вывода с отображением памяти в C
В этом примере код устанавливает значение, хранящееся в фу
к 0
. Затем он начинает опрос это значение несколько раз, пока оно не изменится на 255
:
статический int фу;пустота бар(пустота) { фу = 0; пока (фу != 255) ;}
An оптимизирующий компилятор заметит, что никакой другой код не может изменить значение, хранящееся в фу
, и будем считать, что он останется равным 0
во все времена. Поэтому компилятор заменит тело функции на бесконечный цикл похоже на это:
пустота bar_optimized(пустота) { фу = 0; пока (истинный) ;}
Тем не мение, фу
может представлять местоположение, которое может быть изменено другими элементами компьютерной системы в любое время, например, регистр оборудования устройства, подключенного к ЦПУ. Приведенный выше код никогда не обнаружит такое изменение; без летучий
, компилятор предполагает, что текущая программа - единственная часть системы, которая может изменить значение (что является наиболее распространенной ситуацией).
Чтобы компилятор не оптимизировал код, как указано выше, летучий
ключевое слово используется:
статический летучий int фу;пустота бар (пустота) { фу = 0; пока (фу != 255) ;}
С этой модификацией условие цикла не будет оптимизировано, и система обнаружит изменение, когда оно произойдет.
Как правило, есть барьер памяти операции, доступные на платформах (представленных в C ++ 11), которые должны быть предпочтительнее, чем volatile, поскольку они позволяют компилятору выполнять лучшую оптимизацию и, что более важно, они гарантируют правильное поведение в многопоточных сценариях; ни спецификация C (до C11), ни спецификация C ++ (до C ++ 11) не определяют многопоточную модель памяти, поэтому volatile может не вести себя детерминированно в разных операционных системах / компиляторах / процессорах).[5]
Сравнение оптимизации на C
Следующие программы C и сопутствующие сборки демонстрируют, как летучий
ключевое слово влияет на вывод компилятора. Компилятор в этом случае был GCC.
При наблюдении за кодом сборки хорошо видно, что код, сгенерированный с помощью летучий
объекты более подробны, что делает их длиннее, поэтому характер летучий
объекты могут быть выполнены. В летучий
ключевое слово предотвращает выполнение компилятором оптимизации кода, включающего изменчивые объекты, тем самым гарантируя, что каждое назначение и чтение изменчивой переменной имеет соответствующий доступ к памяти. Без летучий
, компилятор знает, что переменную не нужно перечитывать из памяти при каждом использовании, потому что не должно быть никаких записей в ее ячейку памяти из любого другого потока или процесса.
Сравнение сборок | |
---|---|
Без летучий ключевое слово | С летучий ключевое слово |
# включить | # включить |
gcc -S -O3 -masm = intel noVolatileVar.c -o без.s | gcc -S -O3 -masm = intel VolatileVar.c -o with.s |
.файл "noVolatileVar.c" .intel_syntax без префикса .раздел .rodata.str1.1,"AMS",@progbits,1.LC0: .нить "% d" .раздел .text.startup,"топор",@progbits .p2align 4,,15 .globl главный .тип главный, @ функцияглавный:.LFB11: .cfi_startproc суб rsp, 8 .cfi_def_cfa_offset 16 mov ESI, 110 mov Edi, КОМПЕНСИРОВАТЬ ПЛОСКИЙ:.LC0 xor eax, eax вызов printf mov ESI, 200 mov Edi, КОМПЕНСИРОВАТЬ ПЛОСКИЙ:.LC0 xor eax, eax вызов printf xor eax, eax Добавить rsp, 8 .cfi_def_cfa_offset 8 Ret .cfi_endproc.LFE11: .размер главный, .-главный .ident "GCC: (GNU) 4.8.2" .раздел .note.GNU-стек,"",@progbits | .файл "VolatileVar.c" .intel_syntax без префикса .раздел .rodata.str1.1,"AMS",@progbits,1.LC0: .нить "% d" .раздел .text.startup,"топор",@progbits .p2align 4,,15 .globl главный .тип главный, @ функцияглавный:.LFB11: .cfi_startproc суб rsp, 24 .cfi_def_cfa_offset 32 mov Edi, КОМПЕНСИРОВАТЬ ПЛОСКИЙ:.LC0 mov DWORD PTR [rsp], 10 mov DWORD PTR [rsp+4], 100 mov DWORD PTR [rsp+8], 0 mov DWORD PTR [rsp+12], 0 mov ESI, DWORD PTR [rsp] mov eax, DWORD PTR [rsp+4] Добавить ESI, eax xor eax, eax вызов printf mov eax, DWORD PTR [rsp+4] mov Edi, КОМПЕНСИРОВАТЬ ПЛОСКИЙ:.LC0 mov DWORD PTR [rsp], eax mov eax, DWORD PTR [rsp+4] mov DWORD PTR [rsp+8], eax mov eax, DWORD PTR [rsp+4] mov DWORD PTR [rsp+12], eax mov ESI, DWORD PTR [rsp+8] mov eax, DWORD PTR [rsp+12] Добавить ESI, eax xor eax, eax вызов printf xor eax, eax Добавить rsp, 24 .cfi_def_cfa_offset 8 Ret .cfi_endproc.LFE11: .размер главный, .-главный .ident "GCC: (GNU) 4.8.2" .раздел .note.GNU-стек,"",@progbits |
C ++ 11
Согласно стандарту ISO C ++ 11 ключевое слово volatile предназначено только для доступа к оборудованию; не используйте его для межпотокового взаимодействия. Для межпоточного взаимодействия стандартная библиотека предоставляет std :: atomic
шаблоны.[6]
В Java
В Язык программирования Java также имеет летучий
ключевое слово, но оно используется для несколько иной цели. При применении к полю квалификатор Java летучий
предоставляет следующие гарантии:
- Во всех версиях Java существует глобальный порядок чтения и записи всех изменчивых переменных (это глобальное упорядочение по volatiles является частичным порядком по большему количеству переменных). порядок синхронизации (что является общим порядком по всем действия по синхронизации)). Это означает, что каждый нить доступ к изменчивому полю будет читать его текущее значение перед продолжением вместо (потенциально) использования кэшированного значения. (Однако нет никакой гарантии относительно относительного упорядочения изменчивых чтений и записей с обычными чтениями и записями, что означает, что это обычно бесполезная конструкция потоковой передачи.)
- В Java 5 или новее, изменчивые чтения и записи устанавливают происходит до отношений, очень похоже на получение и освобождение мьютекса.[7]
С помощью летучий
может быть быстрее, чем замок, но он не будет работать в некоторых ситуациях до Java 5[8]. Диапазон ситуаций, в которых действует volatile, был расширен в Java 5; особенно, двойная проверка блокировки теперь работает правильно.[9]
В C #
В C #, летучий
гарантирует, что код, обращающийся к полю, не подвергается некоторым небезопасным для потока оптимизациям, которые могут выполняться компилятором, CLR или аппаратными средствами. Когда поле отмечено летучий
компилятор получает указание создать «барьер памяти» или «забор» вокруг него, который предотвращает переупорядочивание инструкций или кеширование, привязанное к полю. При чтении летучий
поле, компилятор генерирует приобретать забор, что предотвращает перемещение других операций чтения и записи в поле, в том числе в других потоках. перед забор. При письме в летучий
поле, компилятор генерирует выпускной барьер; этот забор предотвращает перемещение других операций чтения и записи в поле после забор.[10]
Можно отметить только следующие типы летучий
: все ссылочные типы, Одинокий
, Булево
, Байт
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
, и все перечисляемые типы с базовым типом Байт
, SByte
, Int16
, UInt16
, Int32
, или же UInt32
.[11] (Это исключает стоимость структуры, а также примитивные типы Двойной
, Int64
, UInt64
и Десятичный
.)
С использованием летучий
ключевое слово не поддерживает поля, которые передано по ссылке или же захваченные локальные переменные; в этих случаях, Thread.VolatileRead
и Thread.VolatileWrite
должен использоваться вместо этого.[10]
Фактически, эти методы отключают некоторые оптимизации, обычно выполняемые компилятором C #, JIT-компилятором или самим процессором. Гарантии, предоставляемые Thread.VolatileRead
и Thread.VolatileWrite
представляют собой надмножество гарантий, предоставляемых летучий
ключевое слово: вместо того, чтобы генерировать «половину забора» (т.е. забор-захват предотвращает только переупорядочение инструкций и кеширование, которое предшествует ему), ЛетучиеЧитать
и VolatileWrite
генерировать «полный забор», который предотвращает переупорядочивание инструкций и кеширование этого поля в обоих направлениях.[10] Эти методы работают следующим образом:[12]
- В
Thread.VolatileWrite
Метод принудительно записывает значение в поле в момент вызова. Кроме того, любые предыдущие загрузки и сохранения программного заказа должны происходить до вызоваVolatileWrite
и любые последующие загрузки и запоминания программного порядка должны происходить после вызова. - В
Thread.VolatileRead
метод заставляет значение в поле считываться из точки вызова. Кроме того, любые предыдущие загрузки и сохранения программного заказа должны происходить до вызоваЛетучиеЧитать
и любые последующие загрузки и запоминания программного порядка должны происходить после вызова.
В Thread.VolatileRead
и Thread.VolatileWrite
методы генерируют полный забор, вызывая Thread.MemoryBarrier
, который создает барьер памяти, работающий в обоих направлениях. В дополнение к мотивам использования полного забора, приведенным выше, есть одна потенциальная проблема с летучий
ключевое слово, которое решается с помощью полного забора, сгенерированного Thread.MemoryBarrier
выглядит следующим образом: из-за асимметричного характера полозаборов летучий
Поле с инструкцией записи, за которой следует инструкция чтения, может по-прежнему иметь замену порядка выполнения компилятором. Поскольку полные заборы симметричны, это не проблема при использовании Thread.MemoryBarrier
. [10]
В Фортране
ЛЕТУЧИЙ
является частью стандарта Fortran 2003,[13] хотя более ранняя версия поддерживала его как расширение. Делаем все переменные летучий
в функции также полезно найти сглаживание связанные ошибки.
целое число, летучий :: я ! Если не определено volatile, следующие две строки кода идентичнызаписывать(*,*) я**2 ! Один раз загружает переменную i из памяти и умножает это значение на самозаписывать(*,*) я*я ! Дважды загружает переменную i из памяти и умножает эти значения
Всегда "углубляясь" в память VOLATILE, компилятор Fortran не может переупорядочивать операции чтения или записи в volatiles. Это делает видимыми для других потоков действия, выполняемые в этом потоке, и наоборот.[14]
Использование VOLATILE сокращает и даже может предотвратить оптимизацию.[15]
Рекомендации
- ^ а б «Публикация комитета по стандартам C ++».
- ^ «Изменчивое ключевое слово в Visual C ++». Microsoft MSDN.
- ^ «Документация по ядру Linux - почему не следует использовать класс типа« volatile »». kernel.org.
- ^ Скотт Мейерс; Андрей Александреску (2004). «C ++ и опасности двойной проверки блокировки» (PDF). DDJ.
- ^ Джереми Эндрюс (2007). "Linux: изменчивые суеверия". kerneltrap.org. Архивировано из оригинал на 2010-06-20. Получено 9 января, 2011.
- ^ "изменчивый (C ++)". Microsoft MSDN.
- ^ Раздел 17.4.4: Порядок синхронизации«Спецификация языка Java®, Java SE 7 Edition». Корпорация Oracle. 2013. Получено 2013-05-12.
- ^ Джереми Мэнсон; Брайан Гетц (февраль 2004 г.). "JSR 133 (модель памяти Java) FAQ". Получено 2019-11-05.
- ^ Нил Коффи. «Двойная проверка блокировки (DCL) и способы ее устранения». Javamex. Получено 2009-09-19.
- ^ а б c d Альбахари, Джозеф. «Часть 4: Продвинутая многопоточность». Потоки в C #. O'Reilly Media. В архиве (PDF) из оригинала 27 апреля 2011 г.. Получено 9 декабря 2019.
- ^ Рихтер, Джеффри (11 февраля 2010 г.). «Глава 7: Константы и поля». CLR через C #. Microsoft Press. стр.183. ISBN 978-0-7356-2704-8.
- ^ Рихтер, Джеффри (11 февраля 2010 г.). «Глава 28: Примитивные конструкции синхронизации потоков». CLR через C #. Microsoft Press. стр.797 –803. ISBN 978-0-7356-2704-8.
- ^ «НЕУСТОЯТЕЛЬНЫЙ атрибут и заявление». Cray.
- ^ «Энергозависимый и разделяемый массив в Фортране». Intel.com.
- ^ "ВОЛАТИЛЬНЫЙ". Oracle.com.