В программировании значение называется изменчивым , если оно может быть прочитано или изменено асинхронно чем-то, кроме текущего потока выполнения . Значение переменной может спонтанно изменяться по таким причинам, как: совместное использование значений с другими потоками; совместное использование значений с асинхронными обработчиками сигналов ; доступ к аппаратным устройствам через отображенный в память ввод-вывод (где вы можете отправлять и получать сообщения с периферийных устройств , считывая и записывая в память). Поддержка этих вариантов использования значительно различается среди нескольких языков программирования, имеющих ключевое слово. Изменчивость может иметь последствия в отношении соглашений о вызове функций и того, как переменные хранятся, доступны и кэшируются.volatile
volatile
В C и C++ volatile
является квалификатором типа , например const
, и является частью типа ( например, типа переменной или поля).
Поведение volatile
ключевого слова в C и C++ иногда дается в терминах подавления оптимизаций оптимизирующего компилятора : 1- не удалять существующие volatile
чтения и записи, 2- не добавлять новые volatile
чтения и записи, и 3- не переупорядочивать volatile
чтения и записи. Однако это определение является лишь приблизительным для удобства новых учащихся, и на это приблизительное определение не следует полагаться при написании реального производственного кода.
В языке C, а следовательно, и в языке C++, volatile
ключевое слово предназначалось для: [1]
longjmp
.volatile
sig_atomic_t
объектах.Стандарты C и C++ позволяют писать переносимый код, который разделяет значения между longjmp
объектами volatile
, а стандарты позволяют писать переносимый код, который разделяет значения между обработчиками сигналов и остальной частью кода в volatile
sig_atomic_t
объектах. Любое другое использование volatile
ключевого слова в C и C++ по своей сути непереносимо или неверно. В частности, написание кода с volatile
ключевым словом для отображаемых в память устройств ввода-вывода по своей сути непереносимо и всегда требует глубоких знаний конкретной целевой реализации C/C++ и платформы.
Распространено заблуждение, что volatile
ключевое слово полезно в переносимом многопоточном коде в C и C++. volatile
Ключевое слово в C и C++ никогда не функционировало как полезный переносимый инструмент для любого многопоточного сценария. [2] [3] [4] [5] В отличие от языков программирования Java и C# , операции над volatile
переменными в C и C++ не являются атомарными , и операции над volatile
переменными не имеют достаточных гарантий упорядочения памяти (т. е. барьеров памяти ). Большинство компиляторов C и C++, компоновщиков и сред выполнения просто не предоставляют необходимых гарантий упорядочения памяти, чтобы сделать volatile
ключевое слово полезным для любого многопоточного сценария. До стандартов C11 и C++11 программисты были вынуждены полагаться на гарантии от отдельных реализаций и платформ (например, POSIX и WIN32) для написания многопоточного кода. С современными стандартами C11 и C++11 программисты могут писать переносимый многопоточный код, используя новые переносимые конструкции, такие как std::atomic<T>
шаблоны. [6]
В этом примере код устанавливает значение, хранящееся в , foo
в 0
. Затем он начинает многократно опрашивать это значение, пока оно не изменится на 255
:
статический int foo ; void bar ( void ) { foo = 0 ; пока ( фу != 255 ) ; }
Оптимизирующий компилятор заметит, что никакой другой код не может изменить значение, хранящееся в foo
, и предположит, что оно 0
всегда будет равно . Поэтому компилятор заменит тело функции бесконечным циклом , подобным этому:
void bar_optimized ( void ) { foo = 0 ; пока ( истина ) ; }
Однако программист может сделать foo
ссылку на другой элемент компьютерной системы, такой как аппаратный регистр устройства, подключенного к ЦП , который может изменить значение foo
во время выполнения этого кода. (В этом примере не рассматриваются подробности того, как сделать foo
ссылку на аппаратный регистр устройства, подключенного к ЦП.) Без volatile
ключевого слова оптимизирующий компилятор , скорее всего, преобразует код из первого образца с чтением в цикле во второй образец без чтения в цикле как часть общей циклически-инвариантной оптимизации кода-движения , и, таким образом, код, скорее всего, никогда не заметит изменения, которого он ждет.
Чтобы запретить компилятору выполнять эту оптимизацию, volatile
можно использовать ключевое слово:
статический изменчивый int foo ; void bar ( void ) { foo = 0 ; пока ( фу != 255 ) ; }
Ключевое volatile
слово не позволяет компилятору вынести чтение за пределы цикла, и, таким образом, код заметит ожидаемое изменение переменной foo
.
Следующие программы на языке C и сопровождающие их фрагменты на языке ассемблера демонстрируют, как volatile
ключевое слово влияет на вывод компилятора. В данном случае компилятором был GCC .
При наблюдении за ассемблерным кодом, ясно видно, что код, сгенерированный с объектами, более многословен, что делает его длиннее, чтобы можно было выполнить volatile
природу объектов. Ключевое слово не позволяет компилятору выполнять оптимизацию кода, включающего volatile-объекты, тем самым гарантируя, что каждое назначение и чтение volatile-переменной имеет соответствующий доступ к памяти. Без ключевого слова компилятор знает, что переменную не нужно перечитывать из памяти при каждом использовании, потому что не должно быть никаких записей в ее ячейку памяти из любого другого потока или процесса.volatile
volatile
volatile
Хотя это и подразумевается как C, так и C++, текущий стандарт C не может выразить, что volatile
семантика относится к lvalue, а не к объекту, на который она ссылается. Соответствующий отчет о дефекте DR 476 (для C11) все еще находится на рассмотрении в C17 . [7]
В отличие от других языковых возможностей C и C++, volatile
ключевое слово не поддерживается большинством реализаций C/C++ - даже для переносимого использования в соответствии со стандартами C и C++. Большинство реализаций C/C++ содержат ошибки в отношении поведения ключевого слова volatile
. [8] [9] Программисты должны быть очень осторожны, когда используют volatile
ключевое слово в C и C++.
Во всех современных версиях языка программирования Java ключевое volatile
слово дает следующие гарантии:
volatile
Чтение и запись являются атомарными . В частности, чтение и запись в поля long
и double
не будут разрываться. ( Гарантия атомарности применяется только к volatile
примитивному значению или volatile
ссылочному значению, а не к любому значению Object.)volatile
чтений и записей. Другими словами, volatile
чтение будет считывать текущее значение (а не прошлое или будущее значение), и все volatile
чтения будут согласовываться с единым глобальным порядком volatile
записей.volatile
Чтения и записи имеют семантику барьера памяти "acquire" и "release" (известную в стандарте Java как happen-before ). [10] [11] Другими словами, volatile
предоставляет гарантии относительного порядка volatile
и не- volatile
чтений и записей. Другими словами, в volatile
основном предоставляет те же гарантии видимости памяти, что и синхронизированный блок Java (но без гарантий взаимного исключения синхронизированного блока ).Вместе эти гарантии превращаются volatile
в полезную многопоточную конструкцию в Java . В частности, типичный алгоритм блокировки с двойной проверкойvolatile
работает правильно в Java . [12]
До Java версии 5 стандарт Java не гарантировал относительного порядка volatile
чтения volatile
и записи. Другими словами, volatile
не имел семантики барьера памяти "acquire" и "release". Это значительно ограничивало его использование в качестве многопоточной конструкции. В частности, типичный алгоритм блокировки с двойной проверкойvolatile
с не работал правильно.
В C# гарантирует volatile
, что код, обращающийся к полю, не будет подвергаться некоторым потоконебезопасным оптимизациям, которые могут быть выполнены компилятором, CLR или оборудованием. Когда поле помечено volatile
, компилятору предписывается сгенерировать «барьер памяти» или «забор» вокруг него, что предотвращает переупорядочивание инструкций или кэширование, привязанное к полю. При чтении volatile
поля компилятор генерирует acquire-fence , который предотвращает перемещение других операций чтения и записи в поле до ограждения. При записи в volatile
поле компилятор генерирует release-fence ; этот забор предотвращает перемещение других операций чтения и записи в поле после ограждения. [13]
Только следующие типы могут быть помечены volatile
: все ссылочные типы, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
и все перечислимые типы с базовым типом Byte
, SByte
, Int16
, UInt16
, Int32
, или UInt32
. [14] (Это исключает структуры значений , а также примитивные типы Double
, Int64
, UInt64
и Decimal
.)
Использование volatile
ключевого слова не поддерживает поля, которые передаются по ссылке или захватываются локальными переменными ; в этих случаях вместо этого Thread.VolatileRead
следует Thread.VolatileWrite
использовать . [13]
По сути, эти методы отключают некоторые оптимизации, обычно выполняемые компилятором C#, JIT-компилятором или самим ЦП. Гарантии, предоставляемые Thread.VolatileRead
и, Thread.VolatileWrite
являются надмножеством гарантий, предоставляемых volatile
ключевым словом: вместо генерации "полузабора" (т. е. забор получения предотвращает только переупорядочивание инструкций и кэширование того, что идет до него), VolatileRead
и VolatileWrite
генерируют "полный забор", который предотвращает переупорядочивание инструкций и кэширование этого поля в обоих направлениях. [13] Эти методы работают следующим образом: [15]
Thread.VolatileWrite
заставляет значение в поле записываться в момент вызова. Кроме того, любые более ранние загрузки и сохранения в порядке программы должны происходить до вызова, VolatileWrite
а любые более поздние загрузки и сохранения в порядке программы должны происходить после вызова.Thread.VolatileRead
заставляет значение в поле считываться из точки вызова. Кроме того, любые более ранние загрузки и сохранения в порядке программы должны происходить до вызова, VolatileRead
а любые более поздние загрузки и сохранения в порядке программы должны происходить после вызова.Методы Thread.VolatileRead
и Thread.VolatileWrite
генерируют полный забор, вызывая Thread.MemoryBarrier
метод, который создает барьер памяти, работающий в обоих направлениях. В дополнение к мотивам использования полного забора, приведенным выше, одна потенциальная проблема с volatile
ключевым словом, которая решается с помощью полного забора, сгенерированного Thread.MemoryBarrier
следующим образом: из-за асимметричной природы полузаборов volatile
поле с инструкцией записи, за которой следует инструкция чтения, все еще может иметь порядок выполнения, измененный компилятором. Поскольку полные заборы симметричны, это не является проблемой при использовании Thread.MemoryBarrier
. [13]
VOLATILE
является частью стандарта Fortran 2003 , [16] хотя более ранняя версия поддерживала его как расширение. Создание всех переменных volatile
в функции также полезно для поиска ошибок, связанных с псевдонимами .
integer , volatile :: i ! Если не определено volatile , следующие две строки кода идентичны write ( * , * ) i ** 2 ! Загружает переменную i один раз из памяти и умножает это значение на само себя write ( * , * ) i * i ! Загружает переменную i дважды из памяти и умножает эти значения
Всегда "погружаясь" в память VOLATILE, компилятор Fortran не может переупорядочивать чтение или запись в volatiles. Это делает видимыми для других потоков действия, выполненные в этом потоке, и наоборот. [17]
Использование VOLATILE снижает и даже может предотвратить оптимизацию. [18]
{{cite web}}
: CS1 maint: бот: исходный статус URL неизвестен ( ссылка )