В C++ есть ключевые слова, окруженные некой аурой загадочности. Просто потому, что используются они редко, в специфических случаях. volatile
, mutable
, typename
, explicit
... Несмотря на редкость использования все равно интересно знать что это такое. Да и вообще как-то неприятно, когда встречаешь и не знаешь что это. volatile
мне встречалось дважды. И оба раза мне не сразу удавалось понять, что же хотел сказать автор. При первом взгляде, это ключевое слово, скорее всего, вызовет какие-то смутные ассоциации: "мммм..... это вроде что-то связанное с потоками" (здесь речь о потоках, которые "thread", а не которые "stream"). На самом деле, указывая volatile
при объявлении переменной, программист просит компилятор не оптимизировать эту переменную.
Компилятор скорее всего оптимизирует код вроде такого, если переменная cancel
не менялась в теле цикла.
bool cancel = false;
while( !cancel ) {
;
}
Если
cancel
не меняется, то ее и проверять каждый раз незачем, компилятор и не будет ее проверять.Зато если вы укажете перед переменной
volatile
, то оптимизиации не будет. Предполагается, что переменная cancel
могла измениться каким-то волшебным образом.
volatile bool cancel = false;
while( !cancel ) {
;
}
Каким волшебным образом? Из другого потока? Но обеспечивать доступ к переменным из разных потоков с помощью
volatile
не есть хорошо. Потому что переменную надо лочить при считывании и записи. Чтобы не получилось, например, такой ситуации: один поток пишет в переменную, другой в этот же момент считывает и считает наполовину старое значение, наполовину новое, короче не пойми что получится. А если ее лочить, то и volatile
не нужен. Доступ к переменным может лочиться по-разному, но по тому, что я читала в форумах, где бы вы ни лочили, volatile
в дополнение к локу указывать не нужно. Более того, использование volatile
в данной ситуации может сказаться на производительности.(Updated 09.01.2009: это не так как минимум для gcc 3.3.6)Чисто теоретически указания
volatile
при работе с потоками достаточно, если тип данных, с которым идет работа, может быть записан на данной архитектуре атомарно, в один прием. Соответственно, надо точно знать к каким именно типам это относится. Казалось бы уж что-что, так bool
должен писаться в один прием. Я вычитала, что на некоторых Windows'ах это вовсе даже и не так. И атомарность присутствует только при работе с char
...Несмотря ни на что,
volatile
таки используется для доступа к переменной из разных потоков. Глобальная переменная объявляется volatile
и вперед. И дважды я встретилась именно с таким использованием volatile
. Но, как уже сказано выше, это не корректно.Update 3.11.2006
volatile в исполнении Микрософт имеет Microsoft Specific пункт. А именно: атомарность операций гарантируется и, как следствие, использование в многопоточных программах приветствуется. Но код получается непортируемым, соответственно.
Я нашла этот пункт только в документации к Visual Studio 2005. Я порылась на MSDN в поисках этого пункта в других версиях Visual Studio, не нашла.
А зачем вообще тогда нужно ключевое слово
volatile
? Джеймс Канзе пишет, что задумывался volatile
для работы с memory mapped IO (MMIO). Интересно он пишет, переведу кусок подробно
На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа туда, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 - различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).
Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:
unsigned char* pControl = 0xff24 ;
*pControl = 0 ;
*pControl = 0 ;
*pControl = 0 ;
Что прекрасно работало на всех компиляторах С. Но вы можете себе представить, что могла бы с этим сделать сама простая оптимизация. По этой причине и нужно было ключевое слово volatile, чтобы сказать компилятору не оптимизировать.
Еще пример из того же обсуждения.
char getChar()
{
static unsigned char const charAvail = 0x01 ;
volatile unsigned char* pStatus = (unsigned char*)0x1234 ;
volatile char* pData = (char*)0x1230 ;
while ( (*pStatus & charAvail) == 0 ) {
}
return *pData ;
}
Без
volatile
переменная pStatus
была бы скорее всего считана лишь один раз на регистр, и работа бы проходила с ее копией на регистре. С volatile
считывание будет проходить в цикле каждый раз.Это все не очень актуально для Windows, Unix систем, там есть свои способы работы с MMIO.
Еще
volatile
, вернее выражение вида volatile sig_atomic_t
, может понадобиться для работы с сигналами. По сигналам я откопала большой FAQ на русском языке, про volatile sig_atomic_t
там есть.volatile
используется с setjmp/longjmp
, для восстановления значения переменных. Это тоже весьма редко используемые функции, и, судя по тому, что я о них читала, компиляторы почти всегда при их использовании все сами могут восстанавливать. Если вы их используете или встретили старый код с ними, то вот, зачем тут может быть volatile
: Volatile type or qualifier?.Кроме
volatile
переменных бывают еще и volatile
функции и volatile
классы. Я знаю лишь один случай их применения. Есть весьма известная и очень критикуемая статья Андрея Александреску volatile - Multithreaded Programmer's Best Friend, где он, используя volatile
, смог добиться безопасной работы с потоками с проверкой на этапе компиляции.Он использует только синтаксические свойства
volatile
. Связь volatile
и оптимизации никак не используется. С тем же успехом он мог бы использовать и const
. Но const
гораздо чаще применяется по своему прямому назначению, чем volatile
, поэтому использование const
создало бы больше неудобств.В начале следующей статьи Андрей суммирует критику Generic: Min and Max Redivivus. По поводу чего критика... Во-первых, он начинает статью с того, что рассказывает о том, как хорошо
volatile
применять при многопоточном программировании. Что, как было сказано выше, вообще говоря неверно. Потом он снимает volatile
с помощью const_cast
с переменной, которая реально является volatile
. Несмотря на то, что скорее всего ничем плохим это не обернется, по Стандарту C++ эта ситуация приводит к undefined behavior. Но все это ничуть не умаляет значимость этой гениальной задумки. Насколько ее удобно использовать в реальном коде это вопрос, но хотя бы с теоретической точки зрения она представляет большой интерес.Вот и все, что мне удалось найти о
volatile
. Пост получился не о том как лучше и удобнее применять volatile
, а скорее о том, что думать, когда вы встретите volatile
в чужом коде. А вообще можно всю жизнь программировать на C++, но с volatile
так и не встретиться.Ссылки по теме:
Подробные и наглядные примеры работы компилятора при указании volatile с диассемблированными примерами кода: раз и два.
comp.lang.c++.moderated: Q: infos, articles, faqs about 'volatile'
comp.lang.c++.moderated: volatile, was: memory visibility between threads - очень большое обсуждение статей Александреску.
comp.lang.c++.moderated: volatile -- what does it mean in relation to member functions?
Doug Harrison Microsoft MVP - Visual C++ об использовании volatile
comp.lang.c++.moderated: Why use volatile anyway?
comp.programming.threads FAQ