энергозависимый (компьютерное программирование)
В компьютерном программировании изменчивость означает , что значение может изменяться со временем вне контроля какого-либо кода. Неустойчивость влияет на соглашения о вызове функций , а также на способ хранения, доступа и кэширования переменных.
В C , C++ , C# и Java языках программирования ключевое Летучее слово указывает, что значение может меняться при различных обращениях, даже если оно не выглядит измененным. Это ключевое слово не позволяет оптимизирующему компилятору оптимизировать последующие операции чтения или записи и, таким образом, неправильно использовать устаревшее значение или пропускать операции записи. Неустойчивые значения в первую очередь возникают при аппаратном доступе ( ввод-вывод с отображением в памяти ), где чтение из памяти или запись в память используется для связи с периферийными устройствами , а также при потоковой обработке , когда другой поток мог изменить значение.
Несмотря на то, что это общее ключевое слово, поведение volatile
существенно различается между языками программирования и его легко понять неправильно. В C и C++ это квалификатор типа , например const
и является свойством типа . Более того, в C и C++ он не работает в большинстве сценариев многопоточности, и его использование не рекомендуется. В Java и C# это свойство переменной и указывает, что объект , к которому привязана переменная, может видоизменяться, и специально предназначен для потоковой обработки. В языке программирования D есть отдельное ключевое слово shared
для использования потоков, но нет volatile
ключевое слово существует.
В С и С++
[ редактировать ]В C и, следовательно, в C++ volatile
Ключевое слово было предназначено для: [1]
- разрешить доступ к ввода-вывода, отображенным в памяти устройствам
- разрешить использование переменных между
setjmp
иlongjmp
- разрешить использование
sig_atomic_t
переменные в обработчиках сигналов.
Поскольку переменные, помеченные как изменчивые, подвержены изменениям за пределами стандартного потока кода, компилятор должен выполнять каждое чтение и запись в переменную, как указано в коде. Любой доступ к изменчивым переменным не может быть оптимизирован, например, с помощью регистров для хранения промежуточных значений.
Хотя стандарты C предназначены как для C, так и для C++, они не отражают, что volatile
семантика относится к lvalue, а не к объекту, на который ссылается. Соответствующий отчет о дефектах DR 476 (до C11) все еще находится на рассмотрении C17 . [2]
Операции над volatile
переменные не являются атомарными и не устанавливают правильных отношений «происходит до» для потоков. Это указано в соответствующих стандартах (C, C++, POSIX , WIN32), [1] и изменчивые переменные не являются потокобезопасными в подавляющем большинстве текущих реализаций. Таким образом, использование volatile
Ключевое слово как переносимый механизм синхронизации не рекомендуется многими группами C/C++. [3] [4] [5]
Пример ввода-вывода с отображением в памяти в C
[ редактировать ]В этом примере код устанавливает значение, хранящееся в foo
к 0
. Затем он начинает многократно опрашивать это значение, пока оно не изменится на 255
:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255)
;
}
Оптимизирующий компилятор заметит, что никакой другой код не может изменить значение, хранящееся в foo
, и будем считать, что оно останется равным 0
всегда. Поэтому компилятор заменит тело функции бесконечным циклом , подобным этому:
void bar_optimized(void) {
foo = 0;
while (true)
;
}
Однако, foo
может представлять собой местоположение, которое может быть изменено другими элементами компьютерной системы в любое время, например, аппаратный регистр устройства, подключенного к ЦП . Приведенный выше код никогда не обнаружит такое изменение; без volatile
ключевое слово, компилятор предполагает, что текущая программа — единственная часть системы, которая может изменить значение (что является наиболее распространенной ситуацией).
Чтобы компилятор не оптимизировал код, как указано выше, volatile
используется ключевое слово:
static volatile int foo;
void bar (void) {
foo = 0;
while (foo != 255)
;
}
При таком изменении состояние контура не будет оптимизировано, и система обнаружит изменение, когда оно произойдет.
Как правило, на платформах доступны операции с барьером памяти (которые представлены в C++11), которым следует отдавать предпочтение вместо энергозависимых, поскольку они позволяют компилятору выполнять лучшую оптимизацию и, что более важно, гарантируют правильное поведение в многопоточных сценариях; ни спецификация C (до C11), ни спецификация C++ (до C++11) не определяют многопоточную модель памяти, поэтому Летучая память может не вести себя детерминированно в разных ОС/компиляторах/ЦП. [6]
Сравнение оптимизации в C
[ редактировать ]Следующие программы на языке C и сопровождающие их фрагменты языка ассемблера демонстрируют, как volatile
Ключевое слово влияет на вывод компилятора. Компилятором в данном случае был GCC .
Наблюдая за ассемблерным кодом, хорошо видно, что код, сгенерированный с помощью volatile
объекты более многословны, что делает их длиннее, поэтому природа volatile
объекты могут быть выполнены. volatile
Ключевое слово не позволяет компилятору выполнять оптимизацию кода, включающего изменчивые объекты, тем самым гарантируя, что каждое назначение и чтение изменчивой переменной имеет соответствующий доступ к памяти. Без volatile
ключевое слово, компилятор знает, что переменную не нужно пересчитывать из памяти при каждом использовании, поскольку не должно быть никаких записей в ее ячейку памяти из любого другого потока или процесса.
Сравнение сборок |
---|
С++11
[ редактировать ]Согласно стандарту ISO C++11 , ключевое слово Летучие предназначено только для использования для доступа к оборудованию; не используйте его для межпотоковой связи. Для межпоточного взаимодействия стандартная библиотека предоставляет std::atomic<T>
шаблоны. [7]
На Яве
[ редактировать ]Язык программирования Java также имеет volatile
ключевое слово, но оно используется для несколько иной цели. При применении к полю квалификатор Java volatile
предоставляет следующие гарантии:
- Во всех версиях Java существует глобальное упорядочение операций чтения и записи всех изменчивых переменных (это глобальное упорядочение изменчивых переменных является частичным порядком по отношению к большему порядку синхронизации (который представляет собой общий порядок по всем действиям синхронизации )). Это означает, что каждый поток, обращающийся к изменяемому полю, будет считывать его текущее значение перед продолжением вместо (потенциально) использования кэшированного значения. (Однако нет никакой гарантии относительного порядка непостоянных операций чтения и записи по отношению к обычным операциям чтения и записи, а это означает, что в целом это бесполезная конструкция потоков.)
- В Java 5 или более поздних версиях изменчивые операции чтения и записи устанавливают отношения «происходит до» , во многом похожие на получение и освобождение мьютекса. [8] [9]
С использованием volatile
может быть быстрее, чем lock , но в некоторых ситуациях он не будет работать до версии Java 5. [10] В Java 5 диапазон ситуаций, в которых эффективен изменчивый код, был расширен; в частности, блокировка с двойной проверкой теперь работает правильно. [11]
В С#
[ редактировать ]В С# , volatile
гарантирует, что код, обращающийся к полю, не подвергается некоторым небезопасным для потоков оптимизации, которые могут выполняться компилятором, средой CLR или аппаратным обеспечением. Когда поле отмечено volatile
, компилятор получает указание создать вокруг него «барьер памяти» или «забор», который предотвращает переупорядочение инструкций или кэширование, привязанное к полю. При чтении volatile
поле, компилятор генерирует Acquire-Fence , который предотвращает перемещение других операций чтения и записи в поле перед ограждением. При письме в volatile
поле компилятор генерирует Release-Fence ; это ограждение предотвращает перемещение других операций чтения и записи в поле после ограждения. [12]
Могут быть отмечены только следующие типы volatile
: все ссылочные типы, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
и все перечислимые типы с базовым типом Byte
, SByte
, Int16
, UInt16
, Int32
, или UInt32
. [13] (Это исключает структуры значений , а также примитивные типы Double
, Int64
, UInt64
и Decimal
.)
Используя volatile
Ключевое слово не поддерживает поля, передаваемые по ссылке или захваченные локальные переменные ; в этих случаях Thread.VolatileRead
и Thread.VolatileWrite
вместо этого необходимо использовать. [12]
По сути, эти методы отключают некоторые оптимизации, обычно выполняемые компилятором C#, JIT-компилятором или самим процессором. Гарантии, предоставляемые Thread.VolatileRead
и Thread.VolatileWrite
являются расширенным набором гарантий, предоставляемых volatile
ключевое слово: вместо создания «полузабора» (т.е. забор захвата предотвращает только переупорядочение и кэширование инструкций, которые предшествуют ему), VolatileRead
и VolatileWrite
создать «полный забор», который предотвращает переупорядочение инструкций и кэширование этого поля в обоих направлениях. [12] Эти методы работают следующим образом: [14]
- The
Thread.VolatileWrite
метод принудительно записывает значение в поле в момент вызова. Кроме того, любые более ранние загрузки и сохранения в программном порядке должны произойти до вызоваVolatileWrite
и любые последующие загрузки и сохранения порядка программы должны происходить после вызова. - The
Thread.VolatileRead
метод принудительно считывает значение в поле в момент вызова. Кроме того, любые более ранние загрузки и сохранения в программном порядке должны произойти до вызоваVolatileRead
и любые последующие загрузки и сохранения порядка программы должны происходить после вызова.
The Thread.VolatileRead
и Thread.VolatileWrite
методы генерируют полный забор, вызывая метод Thread.MemoryBarrier
метод, который создает барьер памяти, работающий в обоих направлениях. В дополнение к мотивам использования полного ограждения, приведенным выше, существует еще одна потенциальная проблема, связанная с volatile
ключевое слово, которое решается с использованием полного ограждения, сгенерированного Thread.MemoryBarrier
заключается в следующем: из-за асимметричности полузаборов volatile
поле с инструкцией записи, за которой следует инструкция чтения, может по-прежнему иметь порядок выполнения, измененный компилятором. Поскольку полные ограждения симметричны, это не является проблемой при использовании Thread.MemoryBarrier
. [12]
На Фортране
[ редактировать ]VOLATILE
является частью стандарта Fortran 2003 , [15] хотя более ранняя версия поддерживала его как расширение. Создание всех переменных volatile
в функции также полезно найти ошибки, связанные с псевдонимами .
integer, volatile :: i ! When not defined volatile the following two lines of code are identical
write(*,*) i**2 ! Loads the variable i once from memory and multiplies that value times itself
write(*,*) i*i ! Loads the variable i twice from memory and multiplies those values
Всегда «детализируя» память VOLATILE, компилятор Fortran не может переупорядочивать операции чтения или записи в VOLATILE. Это делает действия, выполненные в этом потоке, видимыми для других потоков, и наоборот. [16]
Использование VOLATILE снижает и даже может помешать оптимизации. [17]
Ссылки
[ редактировать ]- ^ Jump up to: а б «Публикация комитета по стандартам C++» .
- ^ Краткое описание запроса на разъяснение для C11. Версия 1.13, октябрь 2017 г.
- ^ «Изменчивое ключевое слово в Visual C++» . Microsoft MSDN .
- ^ «Документация по ядру Linux – почему не следует использовать класс типа «летучий»» . ядро.орг .
- ^ Скотт Мейерс; Андрей Александреску (2004). «C++ и опасности блокировки с двойной проверкой» (PDF) . ДДЖ .
- ^ Джереми Эндрюс (2007). «Linux: нестабильное суеверие» . kerneltrap.org. Архивировано из оригинала 20 июня 2010 г. Проверено 9 января 2011 г.
- ^ «летучий (C++)» . Microsoft MSDN .
- ^ Раздел 17.4.4: Порядок синхронизации. «Спецификация языка Java®, Java SE 7 Edition» . Корпорация Оракл . 2013 . Проверено 12 мая 2013 г.
- ^ «Параллелизм Java: понимание «изменчивого» ключевого слова» . dzone.com. 08.03.2021. Архивировано из оригинала 9 мая 2021 г. Проверено 9 мая 2021 г.
- ^ Джереми Мэнсон; Брайан Гетц (февраль 2004 г.). «Часто задаваемые вопросы по JSR 133 (модель памяти Java)» . Архивировано из оригинала 9 мая 2021 г. Проверено 5 ноября 2019 г.
- ^ Нил Коффи. «Блокировка с двойной проверкой (DCL) и способы ее устранения» . Явамекс . Проверено 19 сентября 2009 г.
- ^ Jump up to: а б с д Альбахари, Джозеф. «Часть 4: Расширенная обработка потоков» . Потоки в C# . О'Рейли Медиа. Архивировано из оригинала 12 декабря 2019 года . Проверено 9 декабря 2019 г.
{{cite web}}
: CS1 maint: bot: исходный статус URL неизвестен ( ссылка ) - ^ Рихтер, Джеффри (11 февраля 2010 г.). «Глава 7: Константы и поля». CLR через C# . Майкрософт Пресс. стр. 183 . ISBN 978-0-7356-2704-8 .
- ^ Рихтер, Джеффри (11 февраля 2010 г.). «Глава 28: Примитивные конструкции синхронизации потоков». CLR через C# . Майкрософт Пресс. стр. 797–803 . ISBN 978-0-7356-2704-8 .
- ^ «ИЗМЕНЯЕМЫЙ Атрибут и утверждение» . Крей. Архивировано из оригинала 23 января 2018 г. Проверено 22 апреля 2016 г.
- ^ «Изменчивый и общий массив в Фортране» . Intel.com .
- ^ «ЛЕГКИЙ» . Oracle.com .