В компьютерном программировании , особенно в C , C ++ , C # и Java Языки программирования , то летучее ключевое слово указывает на то, что значение может изменяться между различными доступами, даже если она не кажется , быть изменен. Это ключевое слово предотвращает оптимизирующий компилятор от оптимизации последующих операций чтения или записи и, таким образом, неправильного повторного использования устаревшего значения или исключения записи. Изменчивые значения в основном возникают при доступе к оборудованию ( ввод-вывод с отображением в память ), где чтение или запись в память используются для связи с периферийными устройствами , а также при потоковой передаче., где другой поток мог изменить значение.
Несмотря на то, что ключевое слово является общим, его поведение volatile
значительно различается между языками программирования, и его легко неправильно понять. В C и C ++ это квалификатор типа , например const
, и свойство типа . Более того, в C и C ++ он не работает в большинстве сценариев многопоточности, и такое использование не рекомендуется. В Java и C # это свойство переменной, указывающее, что объект, к которому привязана переменная, может изменяться и специально предназначен для многопоточности. В языке программирования D есть отдельное ключевое слово shared
для использования потоковой передачи, но не volatile
существует ключевого слова.
В C и C ++
В C и, следовательно, C ++ это volatile
ключевое слово предназначалось для [1]
- разрешить доступ к устройствам ввода-вывода с отображением памяти
- разрешить использование переменных между
setjmp
иlongjmp
- разрешить использование
sig_atomic_t
переменных в обработчиках сигналов.
Операции с volatile
переменными не являются атомарными и не устанавливают правильную связь «происходит до» для потоковой передачи. Это указано в соответствующих стандартах (C, C ++, POSIX , WIN32), [1] и изменчивые переменные не являются потокобезопасными в подавляющем большинстве текущих реализаций. Таким образом, использование volatile
ключевого слова в качестве переносимого механизма синхронизации не одобряется многими группами C / C ++. [2] [3] [4]
Пример ввода-вывода с отображением памяти в C
В этом примере код устанавливает значение , хранящееся в foo
с 0
. Затем он начинает многократно опрашивать это значение, пока оно не изменится на 255
:
статический int foo ;пустая панель ( void ) { foo = 0 ; в то время как ( foo ! = 255 ) ; }
Оптимизирующий компилятор заметит , что никакой другой код не может , возможно , изменить значение , сохраненное в foo
, и будем считать , что он будет оставаться равным 0
во все времена. Поэтому компилятор заменит тело функции бесконечным циклом, подобным следующему:
void bar_optimized ( void ) { foo = 0 ; в то время как ( правда ) ; }
Однако foo
может представлять местоположение, которое может быть изменено другими элементами компьютерной системы в любое время, например, аппаратный регистр устройства, подключенного к ЦП . Приведенный выше код никогда не обнаружит такое изменение; без volatile
ключевого слова компилятор предполагает, что текущая программа - единственная часть системы, которая может изменить значение (что, безусловно, является наиболее распространенной ситуацией).
Чтобы компилятор не оптимизировал код, как указано выше, используется volatile
ключевое слово:
статический изменчивый int foo ;пустая панель ( void ) { foo = 0 ; в то время как ( foo ! = 255 ) ; }
С этой модификацией условие цикла не будет оптимизировано, и система обнаружит изменение, когда оно произойдет.
Как правило, на платформах (которые представлены в C ++ 11) доступны операции с ограничением памяти, которые должны быть предпочтительнее, чем volatile, поскольку они позволяют компилятору выполнять лучшую оптимизацию и, что более важно, они гарантируют правильное поведение в многопоточных сценариях; ни спецификация C (до C11), ни спецификация C ++ (до C ++ 11) не определяют многопоточную модель памяти, поэтому volatile не может детерминированно вести себя в разных операционных системах / компиляторах / процессорах). [5]
Сравнение оптимизации на C
Следующие программы C и сопутствующие сборки демонстрируют, как volatile
ключевое слово влияет на вывод компилятора. Компилятором в данном случае был GCC .
При наблюдении за кодом сборки ясно видно, что код, сгенерированный с помощью volatile
объектов, более подробный, что делает его длиннее, чтобы volatile
можно было реализовать природу объектов. В volatile
ключевых словах , компилятор от выполнения оптимизации по коде с участием летучих объектов, тем самым гарантируя , что каждое летучее назначение и чтение переменного имеют соответствующий доступ к памяти. Без volatile
ключевого слова компилятор знает, что переменную не нужно перечитывать из памяти при каждом использовании, потому что не должно быть никаких записей в ее ячейку памяти из любого другого потока или процесса.
Сравнение сборок | |
---|---|
Без volatile ключевого слова | С volatile ключевым словом |
# включить | # включить |
gcc -S -O3 -masm = intel noVolatileVar.c -o без.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 главного .типа главного , @function главного: .LFB11: .cfi_startproc суб RSP , 8 .cfi_def_cfa_offset 16 мов ЭРИ , 110 мов EDI , OFFSET FLAT :. LC0 xor eax , вызов eax printf mov esi , 200 mov edi , OFFSET FLAT :. LC0 XOR EAX , EAX вызов Printf XOR EAX , EAX добавить RSP , 8 .cfi_def_cfa_offset 8 RET .cfi_endproc .LFE11: .size главное , . Главным .ident "GCC: (ГНУ) 4.8.2" .section .note.GNU- стек , "" , @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 главного .типа главного , @function главного: .LFB11: .cfi_startproc суб RSP , 24 .cfi_def_cfa_offset 32 мов EDI , OFFSET FLAT :. 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 ] 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 C ++ 11 ключевое слово volatile предназначено только для доступа к оборудованию; не используйте его для межпоточного взаимодействия. Для межпотокового взаимодействия стандартная библиотека предоставляет std::atomic
шаблоны. [6]
В Java
В языке программирования Java также есть volatile
ключевое слово, но оно используется для несколько иной цели. При применении к полю квалификатор Java volatile
обеспечивает следующие гарантии:
- Во всех версиях Java существует глобальный порядок чтения и записи всех изменчивых переменных (это глобальное упорядочение по volatiles является частичным порядком по большему порядку синхронизации (который является общим порядком по всем действиям синхронизации )). Это означает, что каждый поток, обращающийся к изменчивому полю, будет читать свое текущее значение перед продолжением вместо (потенциально) использования кэшированного значения. (Однако нет никакой гарантии относительно относительного упорядочения изменчивых операций чтения и записи с обычными операциями чтения и записи, что означает, что это обычно бесполезная конструкция потоковой передачи.)
- В Java 5 или более поздних версиях изменчивые операции чтения и записи устанавливают связь « происходит раньше» , подобно получению и освобождению мьютекса. [7] [8]
Использование volatile
может быть быстрее, чем блокировка , но оно не будет работать в некоторых ситуациях до Java 5. [9] Диапазон ситуаций, в которых действует volatile, был расширен в Java 5; в частности, теперь корректно работает блокировка с двойной проверкой . [10]
В C #
В C # , volatile
гарантирует , что код доступа поле не подпадает под некоторым нит небезопасным оптимизации , которые могут быть выполнены компилятором, в CLR, или с помощью аппаратных средств. Когда поле помечено volatile
, компилятор получает указание создать «барьер памяти» или «ограждение» вокруг него, что предотвращает переупорядочение инструкций или кеширование, привязанное к полю. При чтении volatile
поля компилятор генерирует забор-забор , который предотвращает перемещение других операций чтения и записи в поле, в том числе в других потоках, перед забором. При записи в volatile
поле компилятор генерирует ограничение выпуска ; это ограждение предотвращает перемещение других операций чтения и записи в поле после ограждения. [11]
Только следующие типы могут быть отмечены volatile
: все ссылочные типы, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
, и все перечисленные типы с лежащей в основе типа Byte
, SByte
, Int16
, UInt16
, Int32
, или UInt32
. [12] (Это исключает значение Структуры , а также типы примитивов Double
, Int64
, UInt64
и Decimal
.)
Использование volatile
ключевого слова не поддерживает поля, которые передаются по ссылке или фиксируются локальные переменные ; в этих случаях Thread.VolatileRead
и Thread.VolatileWrite
должны использоваться вместо этого. [11]
Фактически, эти методы отключают некоторые оптимизации, обычно выполняемые компилятором C #, JIT-компилятором или самим процессором. Гарантии, предоставляемые ключевым словом, Thread.VolatileRead
и Thread.VolatileWrite
являются надмножеством гарантий, предоставляемых volatile
ключевым словом: вместо создания «полузащиты» (т. Е. Захват-ограждение только предотвращает переупорядочение инструкций и кеширование, которое предшествует ему) VolatileRead
и VolatileWrite
генерировать «полное ограждение», которое предотвратить переупорядочение инструкций и кеширование этого поля в обоих направлениях. [11] Эти методы работают следующим образом: [13]
- В
Thread.VolatileWrite
методе сила значения в поле , чтобы быть записана в момент вызова. Кроме того, любые более ранние загрузки и сохранения программного порядка должны происходить до вызова,VolatileWrite
а любые последующие загрузки и сохранения программного порядка должны происходить после вызова. - В
Thread.VolatileRead
метод силы значение в поле , чтобы быть считаны с в точке вызова. Кроме того, любые более ранние загрузки и сохранения программного порядка должны происходить до вызова,VolatileRead
а любые последующие загрузки и сохранения программного порядка должны происходить после вызова.
Thread.VolatileRead
И Thread.VolatileWrite
методы создание полного забора с помощью вызова Thread.MemoryBarrier
метода, который строит барьер памяти , который работает в обоих направлениях. В дополнение к приведенным выше мотивам использования полного ограждения, одна потенциальная проблема с volatile
ключевым словом, которая решается с помощью полного ограждения, сгенерированного с помощью, Thread.MemoryBarrier
заключается в следующем: из-за асимметричной природы полузаборов, volatile
поле с инструкцией записи, за которой следует инструкция чтения может все еще иметь порядок выполнения, замененный компилятором. Поскольку полные заборы симметричны, это не проблема при использовании Thread.MemoryBarrier
. [11]
В Фортране
VOLATILE
является частью Fortran 2003 стандарта, [14] , хотя более ранняя версия поддерживает его в качестве расширения. Создание всех переменных volatile
в функции также полезно для поиска ошибок, связанных с псевдонимом .
целое , изменчивое :: i ! Если volatile не определен, следующие две строки кода идентичны write ( * , * ) i ** 2 ! Один раз загружает переменную i из памяти и умножает это значение на само значение write ( * , * ) i * i ! Дважды загружает переменную i из памяти и умножает эти значения
Всегда "углубляясь" в память VOLATILE, компилятор Fortran не может переупорядочивать чтение или запись в volatiles. Это делает видимыми для других потоков действия, выполняемые в этом потоке, и наоборот. [15]
Использование VOLATILE сокращает и даже может предотвратить оптимизацию. [16]
Рекомендации
- ^ a b «Публикация комитета по стандартам 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 . Проверено 12 мая 2013 .
- ^ «Java Concurrency: понимание ключевого слова« изменчивой »» . dzone.com. 2021-03-08. Архивировано из оригинала на 2021-05-09 . Проверено 9 мая 2021 .
- ^ Джереми Мэнсон; Брайан Гетц (февраль 2004 г.). «JSR 133 (модель памяти Java): часто задаваемые вопросы» . Архивировано из оригинала на 2021-05-09 . Проверено 5 ноября 2019 .
- ^ Нил Коффи. «Двойная проверка блокировки (DCL) и как это исправить» . Javamex . Проверено 19 сентября 2009 .
- ^ а б в г Альбахари, Джозеф. «Часть 4: Продвинутая многопоточность» . Многопоточность в C # . O'Reilly Media. Архивировано 27 апреля 2011 года (PDF) . Проверено 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 .
Внешние ссылки
- Справочное руководство Ada C.6: Управление общими переменными
- Ядро Linux: нестабильное-опасное