В компьютерном программировании, особенно в C, C ++, C# и Java языков программирования, ключевое слово volatile указывает, что значение может меняться при разных доступах, даже если оно не кажутся измененными. Это ключевое слово предотвращает оптимизирующий компилятор от оптимизации последующих операций чтения или записи и, таким образом, неправильного повторного использования устаревшего значения или исключения записи. Неустойчивые значения в первую очередь возникают при доступе к оборудованию (ввод-вывод с отображением в память ), где чтение или запись в память используются для связи с периферийными устройствами, а в потоковой передаче, где другой поток мог изменить значение.
Несмотря на то, что это общее ключевое слово, поведение volatile
значительно различается между языками программирования, и его легко понять неправильно. В C и C ++ это квалификатор типа , например const
, и свойство типа . Более того, в C и C ++ он не работает в большинстве сценариев многопоточности, и такое использование не рекомендуется. В Java и C # это свойство переменной и указывает, что объект , к которому привязана переменная, может изменяться и специально предназначен для потоковой передачи. В языке программирования D есть отдельное ключевое слово shared
для использования потоковой передачи, но ключевое слово volatile
отсутствует.
В C и, следовательно, C ++, ключевое слово volatile
был предназначен для
setjmp
и longjmp
sig_atomic_t
в обработчиках сигналов.Операции с переменными volatile
не являются атомарными, и они не устанавливают правильное выполнение - перед отношениями для многопоточности. Это указано в соответствующих стандартах (C, C ++, POSIX, WIN32), и изменчивые переменные не являются потокобезопасными в подавляющем большинстве текущих реализаций. Таким образом, использование ключевого слова volatile
в качестве переносимого механизма синхронизации не приветствуется многими группами C / C ++.
In В этом примере код устанавливает значение, хранящееся в foo
, равным 0
. Затем он начинает опрашивать это значение несколько раз, пока оно не изменится на 255
:
static int foo; пустая панель (void) {foo = 0; в то время как (foo! = 255); }
Оптимизирующий компилятор заметит, что никакой другой код не сможет изменить значение, хранящееся в foo
, и предположит, что оно останется равным 0
всегда. Поэтому компилятор заменит тело функции бесконечным циклом , подобным этому:
void bar_optimized (void) {foo = 0; пока (правда); }
Однако foo
может представлять местоположение, которое может быть изменено другими элементами компьютерной системы в любое время, например, аппаратный регистр устройства, подключенного к ЦП. Приведенный выше код никогда не обнаружит такое изменение; без ключевого слова volatile
компилятор предполагает, что текущая программа - единственная часть системы, которая может изменить значение (что является наиболее распространенной ситуацией).
Чтобы компилятор не оптимизировал код, как указано выше, используется ключевое слово volatile
:
static volatile int foo; пустая панель (void) {foo = 0; в то время как (foo! = 255); }
С этой модификацией условие цикла не будет оптимизировано, и система обнаружит изменение, когда оно произойдет.
Как правило, на платформах (которые представлены в C ++ 11) доступны операции с ограничением памяти, которые должны быть предпочтительнее, чем изменчивые, поскольку они позволяют компилятору выполнять лучшую оптимизацию и многое другое. что важно, они гарантируют правильное поведение в многопоточных сценариях; ни спецификация C (до C11), ни спецификация C ++ (до C ++ 11) не определяет многопоточную модель памяти, поэтому volatile может не вести себя детерминированно в разных операционных системах / компиляторах / процессорах).
Следующие программы на C и сопутствующие сборки демонстрируют, как ключевое слово volatile
влияет на вывод компилятора. Компилятором в данном случае был GCC.
. При наблюдении за кодом сборки ясно видно, что код, созданный с помощью объектов volatile
, более подробный, что делает его длиннее, поэтому характер volatile
объекты могут быть выполнены. Ключевое слово volatile
предотвращает выполнение компилятором оптимизации кода, включающего изменчивые объекты, тем самым гарантируя, что каждое назначение и чтение изменчивой переменной имеет соответствующий доступ к памяти. Без ключевого слова volatile
компилятор знает, что переменную не нужно перечитывать из памяти при каждом использовании, потому что не должно быть никаких записей в ее ячейку памяти из любого другого потока или процесса.
Сравнение сборок | |
---|---|
Без volatile keyword | С volatile ключевое слово |
# include | # include |
gcc -S -O3 -masm = intel noVolatileVar.c -o without.s | gcc -S -O3 -masm = intel VolatileVar.c -o with.s |
.file "noVolatileVar.c ".intel_syntax noprefix.section.rodata.str1.1," aMS ", @ progbits, 1.LC0:.string"% d ".section.text.startup," ax ", @ progbits.p2align 4,, 15. globl main.type main, @function main:.LFB11:.cfi_startproc sub rsp, 8.cfi_def_cfa_offset 16 mov esi, 110 mov edi, OFFSET FLAT:.LC0 xor eax, eax call printf mov esi, 200 mov edi, OFFSET FLAT:.LC0 xor eax, eax call printf xor eax, eax add rsp, 8.cfi_def_cfa_offset 8 ret.cfi_endproc.LFE11:.size main,.-Main.ident "GCC: (GNU) 4.8.2".section.note.GNU -stack, "", @ progbits | .file "VolatileVar.c".intel_syntax noprefix.section.rodata.str1.1, "aMS", @ progbits, 1.LC0:.string "% d".section. text.startup, "ax", @ progbits.p2align 4,, 15.globl main.type main, @function main:.LFB11:.cfi_startproc sub rsp, 24.cfi_def_cfa_offset 32 mov edi, OFFSET FLAT:.LC0 mov DWORD PTR [rsp], 10 mov DWORD PTR [rsp + 4], 100 mov D WORD PTR [rsp + 8], 0 mov DWORD PTR [rsp + 12], 0 mov esi, DWORD PTR [rsp] mov eax, DWORD PTR [rsp + 4] add esi, eax xor eax, eax call printf mov eax, DWORD PTR [rsp + 4] mov edi, OFFSET FLAT:.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] add esi, eax xor eax, eax call printf xor eax, eax add rsp, 24.cfi_def_cfa_offset 8 ret.cfi_endproc.LFE11:.size main,.-Main.ident "GCC: (GNU) 4.8.2".section.note.GNU-stack, "", @ progbits |
Согласно стандарту C ++ 11 ISO ключевое слово volatile предназначено только для использования для доступа к оборудованию; не используйте его для межпотокового взаимодействия. Для межпотокового взаимодействия стандартная библиотека предоставляет шаблоны std::atomic
.
Язык программирования Java также имеет ключевое слово volatile
, но используется для несколько иной цели. При применении к полю квалификатор Java volatile
обеспечивает следующие гарантии:
Использование volatile
может быть быстрее, чем блокировка, но не будет работать в некоторые ситуации до Java 5. Диапазон ситуаций, в которых действует volatile, был расширен в Java 5; в частности, блокировка с двойной проверкой теперь работает правильно.
In C#, volatile
гарантирует, что код доступа к полю не подвергается некоторым поточно-небезопасные оптимизации, которые могут выполняться компилятором, CLR или аппаратно. Когда поле помечено volatile
, компилятор получает указание создать «барьер памяти» или «ограждение» вокруг него, что предотвращает переупорядочение инструкций или кеширование, привязанное к полю. При чтении поля volatile
компилятор генерирует блокировку захвата, которая предотвращает перемещение других операций чтения и записи в поле, в том числе в других потоках, перед блокировкой. При записи в поле volatile
компилятор генерирует ограничение выпуска; это ограждение предотвращает перемещение других операций чтения и записи в поле после ограждения.
Только следующие типы могут быть помечены volatile
: все ссылочные типы, Single
, Логический
, Байт
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
и все перечисляемые типы с базовым типом Byte
, SByte
, Int16
, UInt16
, Int32
или UInt32
. (Это исключает структуры значения , а также примитивные типы Double
, Int64
, UInt64
и Decimal
.)
Использование ключевого слова volatile
не поддерживает поля, которые передаются по ссылке или захваченные локальные переменные ; в этих случаях вместо них должны использоваться Thread.VolatileRead
и Thread.VolatileWrite
.
По сути, эти методы отключают некоторые оптимизации, обычно выполняемые компилятором C #, JIT-компилятор или сам процессор. Гарантии, предоставляемые Thread.VolatileRead
и Thread.VolatileWrite
, представляют собой надмножество гарантий, предоставляемых ключевым словом volatile
: вместо создания "полузаборы" ( т.е. блокировка получения предотвращает только переупорядочение инструкций и кеширование, которое предшествует этому), VolatileRead
и VolatileWrite
генерируют "полную блокировку", которая предотвращает переупорядочение инструкций и кеширование этого поля в обоих направлениях. Эти методы работают следующим образом:
Thread.VolatileWrite
принудительно записывает значение в поле в момент вызова. Кроме того, любые более ранние загрузки и сохранения программного порядка должны происходить до вызова VolatileWrite
, а любые последующие загрузки и сохранения программного порядка должны происходить после вызова.Thread.VolatileRead
приводит к принудительному считыванию значения в поле в момент вызова. Кроме того, любые более ранние загрузки и сохранения программного порядка должны происходить до вызова VolatileRead
, а любые последующие загрузки и сохранения программного порядка должны происходить после вызова.Thread.VolatileRead
и Thread.VolatileWrite
генерируют полный забор путем вызова метода Thread.MemoryBarrier
, который создает барьер памяти, работающий в обоих направлениях. В дополнение к приведенным выше мотивам использования полного ограждения, одна потенциальная проблема с ключевым словом volatile
, которая решается путем использования полного ограждения, сгенерированного Thread.MemoryBarrier
, заключается в следующем: due Из-за асимметричной природы полузаборов, поле volatile
с инструкцией записи, за которой следует инструкция чтения, может все еще иметь порядок выполнения, измененный компилятором. Поскольку полные ограждения симметричны, это не проблема при использовании Thread.MemoryBarrier
.
VOLATILE
является частью стандарта Fortran 2003, хотя более ранняя версия поддерживала его как расширение. Создание всех переменных volatile
в функции также полезно для поиска ошибок, связанных с псевдонимом.
целое, изменчивое :: i! Если volatile не определен, следующие две строки кода идентичны write (*, *) i ** 2! Один раз загружает переменную i из памяти и умножает это значение на само значение write (*, *) i * i! Дважды загружает переменную i из памяти и умножает эти значения.
Всегда "углубляясь" в память VOLATILE, компилятор Fortran не может переупорядочивать чтение или запись в volatile. Это делает видимыми для других потоков действия, выполняемые в этом потоке, и наоборот.
Использование VOLATILE уменьшает и даже может предотвратить оптимизацию.