В 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

Atom feed
29 коммент.:
Интересно то как...
>>где бы вы ни лочили, volatile в дополнение к локу указывать не нужно
Помоему это неверно. volatile нужно указывать в дополнение к локу так как иначе возможна например такая ситуация:
Два потока работают с одной переменной, они используют локи для синхронизации.
Локи гарантируют нам что ошибки связанной с чтением частично измененной переменной небудет, но оптимизатор мог поместить переменную в регист, и тогда второй поток не увидит изменений сделанных первым. Без volatile потоки могут иметь копии переменной в регистре.
Согласен с предыдущим автором.
Volatile не прихоть а суровая необходимость. Критические секции гарантируют не одновременный доступ потоков к объекту. Но компилятор ничего не знает о них и может переменную вообще не разу не прочитать, а в место вашего цикла поставить один jamp.
(И это может стать заметно только в Release'е, где будет работать -O2 )
Интересно то как...
:-)
Помоему это неверно. volatile нужно указывать в дополнение к локу
Тут я ориентируюсь, по тому, что читала в comp.lang.c++.moderated. Там обсуждались локи в разных системах, локи разные, где что используется. И в итоге вывод такой: лок - это некая внешняя функция, которая, теоретически, могла поменять значение переменной. Поэтому переменная обязана была быть считана заново. Я пороюсь еще и ссылки укажу, где об этом говориться. Ни одной системы, где дополнительно обязательно было бы нужно указывать volatile народ так и не упомянул.
Я так понимаю, что критичеcкие секции, как правило, применяются один раз в функции, а значит и переменные меняющиеся внутри секции должны считываться из памяти! Вот если в одной функции блокировка применяется дважды и дважды идет работа с переменной - тогда компилятор без volatile может и с оптимизировать :).
Очень понравился класс LockingPtr у Andrei Alexandrescu. Возражения по поводу некорректности сильно надуманны. Не знаю, как может не работать mutex, но в крайнем случае класс легко переписать на использование тех же memory barriers. Далее, то что система может куда то перекидывать данные в зависимости от определения volatile - покажите мне эту систему! Ну и для реализации более хитрых блокировок нужно просто писать более хитрые классы.
Вот если в одной функции блокировка применяется дважды и дважды идет работа с переменной - тогда компилятор без volatile может и с оптимизировать
Вот обещанные ссылки по этому поводу: 1, 2, 3, 4. Несмотря на то, что им отвечают, что теоретически возможны системы, где без volatile не обойтись, все на этом "теоретически" и останавливается. Конкретных примеров нет.
А еще вы забыли про DOS. Реальной многопоточности там не было. Но были прерывания. И все данные которые мог поменять обработчик прерывания надо было помечать как volatile.
Пример?! Их есть у меня! :)
static vector < char > buffer_;
static Mutex mtx_;
static Event ev1_, ev2_;
void func1() {
mtx_.Lock();
buffer_.resize(100);
; //Здесь должна быть загрузка данных в вектор
mtx_.Unlock();
ev1_.SetEvent();
WaitForSingleObject(ev2_,INFINITE);
ev1_.ResetEvent();
mtx_.Lock();
; //Здесь должна быть выгрузка данных из вектора
mtx_.Unlock();
};
Здесь 1 поток загружает данные в вектор buffer_ для 2 потока и устанавливает событие ev1. Далее он ждет пока 2 поток обработает данные вектора и туда же запишет свой ответ. После чего идет анализ полученных данных.
В результате размер вектора меняется, и может поменяться ссылка на данные вектора! Если компилятор при первой блокировке с оптимизирует эту ссылку в регистр, то прога может легко вылететь!
Я согласен что пример корявый (я так не пишу :) ). Но в принципе он корректен. Есть много возможностей написать лучше, НО - пусть тот кто всегда писал красиво и стильно первым бросит в меня камень!
Все будет работать если исправить первую строчку на:
volatile static vector < char > buffer_;
Однако обращение к buffer_ не оптимально. Если же использовать LockingPtr, как
void func1() {
{ LockingPtr < vector < char > > lpBuf(buffer_, mtx_);
lpBuf.resize(100);
; //Здесь должна быть загрузка данных в вектор
};
ev1_.SetEvent();
WaitForSingleObject(ev2_,INFINITE);
ev1_.ResetEvent();
{ LockingPtr < vector < char > > lpBuf(buffer_, mtx_);
; //Здесь должна быть выгрузка данных из вектора
};
};
То компилятор сможет правильно оптимизировать обращение к переменной buffer_.
PS.
С моей точки LockingPtr хорош прежде всего тем, что не дает забыть про блокировку объекта. Такую ошибку легко сделать и потом обнаружить уже после сдачи программы.
А еще вы забыли про DOS. Реальной многопоточности там не было. Но были прерывания. И все данные которые мог поменять обработчик прерывания надо было помечать как volatile.
Интересно, не знала об этом.
В результате размер вектора меняется, и может поменяться ссылка на данные вектора! Если компилятор при первой блокировке с оптимизирует эту ссылку в регистр, то прога может легко вылететь!
Вот это и есть отстутсвие конкретного примера. Это лишь догадки. Вот ссылка на стандарт, документацию, прояснила бы ситуацию.
Теоретически может быть, что где-то, на какой-то системе, такое положение вещей и возможно. Никто не мешает любому из присутствующих написать ОС и компилятор где все будет работать именно так как предложено.
Переведу все же то, что по этому поводу пишет Канзе.
"... только один поток в данный момент времени имеет доступ к переменной, защищенной мьютексом. И освобождение, и захват мьютекса (по крайней мере на POSIX системах) гарантирует барьеры памяти: когда процесс A освобождает мьютекс, гарантировано, что все предыдущие изменения видны глобально, и когда процесс B захватывает мьютекс, гарантированно, что он будет видеть все глобально видимые изменения, которые предшествовали захвату. (Замечу, что оба условия необходимы. То, что изменения видимы глобально не означает, что другие процессы будут использовать глобальное состояние, а не их собственное.)".
Далее по ходу обсуждения он приводит ссылку
"In Posix (IEEE Std 1003.1, Base Definitions, General Concepts, Memory
Synchronization): "The following functions synchronize memory with
respect to other threads: [...]". Both pthread_mutex_lock and
pthread_mutex_unlock are in the list. "
Если я не верю Канзе, я могу сама стандарт почитать: The Single UNIX Specification Version 3.
Судя по примеру кода речь идет о WinAPI. Я не знаю, какие гарантии дает в таких ситуациях WinAPI. Возможно такие же, возможно нет.
volatile используется практически во всех Unix daemons как переменная, которую можно установить в обработчике сигнала.
Рекомендую к прочтению http://linuxdevices.com/articles/AT5980346182.html
статья понравилась, но...
volatile unsigned char* pStatus = (unsigned char*)0x1234 ;
while ( *pStatus ...){...}
в Win9x и далее первые 64K отведены как раз для отлавливания "ошибок Досовской молодости", и обращение к памяти по этому адрессу вызывает exception, следовательно сколько раз отработает while с volatile и без него да ни сколько :))
Volatile часто используется при системном программировании, особенно при разработке драйверов. Она заставляет компилятор не оптимизировать переменные, которые могут поменяться по какой-либо внешней причине, неизвестной для компилятора. Обычно это бывает возникновение прерывания или поступление информации в порт - его компилятор отследить не может, поэтому ему надо запретить оптимизацию (т.е. изменение) переменных, в которых эти данные должны поступать.
Я как-то использовал volatile чтобы умерить пыл багландовского оптимизатора. Без этого модификатора, он даже в отладочном режиме имеет обыкновение выкидывать переменные сразу после последнего использования. ;-(
Алёна, спасибо за статью!
Очень полезно! Как раз то, что я искал!
The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread. (http://msdn.microsoft.com/en-us/library/12a04hfd.aspx)
И не надо ничего более выдумывать.
2napa3um
The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware,
Фраза, которая вызывает много вопросов. Например, могут ли объекты быть использованы таким образом, если volatile не указан.
or a concurrently executing thread.
Ну вот это самодеятельность Микрософта. До того как будет принят новый Стандарт, по крайней мере.
И не надо ничего более выдумывать.
Да собственно практически все посты по cpp, что я пишу, выдумок не содержат. В основном это переведенные обсуждения из comp.lang.c++.moderated и куски из последнего Стандарта.
Мой коменатрий о Ваших выдумках касался фразы "А если ее лочить, то и volatile не нужен.", и далее по тексту. Так вот, volatile не обеспечивает монопольного блокирования переменной. Потому средства синхронизации (к примеру, мютекс) для доступа к этой переменной из разных потоков не отменяются. Но эти средства синхронизации ни в коем случае не отменяют необходимость в слове volatile. Пример (утрированный):
do {
pMutex->Lock();
int iLocalVar = iSharedVar;
pMutex->Unlock();
MyFun();
} while(iLocalVar);
После оптимизации этот цикл может выполнятся вечно, т.к. реальное чтение переменной iSharedVar может "заоптимизироваться" и быть вынесено за цикл. Суть в том, что мютексы - это сущности не языка (также, как и потоки), это сущности операционной системы. Без volatile компилятор "не догадается", как ему компилировать обращения к разделяемым между потоками (файберами, прерываниями и т.п.) переменным, даже если это обращение "обёрнуто" в вызовы мютекса Lock/Unlock.
В общем, я лишь присоединяюсь к мнению анонимного автора самого первого комментария, и всех тех, кто к нему присоединились. Volatile - это не экзотика, а очень распространённое в реальных программах (многопоточные демоны) явление.
И в итоге вывод такой: лок - это некая внешняя функция, которая, теоретически, могла поменять значение переменной.
Как компилятор узнает в моём предыдущем примере, что мютекс и iSharedVar чем-то связаны?
2napa3um
Так вот, volatile не обеспечивает монопольного блокирования переменной.
Угу
Потому средства синхронизации (к примеру, мютекс) для доступа к этой переменной из разных потоков не отменяются.
угу
Но эти средства синхронизации ни в коем случае не отменяют необходимость в слове volatile.
Вот это у меня вызывает сомнения. Сомнения я излагала раньше вот тут.
Комментарий от "OU" ("OU-a30ua")..
"Ага.."
Тут все вроде как в основном С++ "чистые программисты" комментарии составляли.
В Embedded (т.е. в программном обеспечении для "железа" (т.е. devices или/и систем на их основе))
в некоторых случаях не обойтись без volatile.
Один из них - когда указывается переменная, которая используется как адрес (порта, устройства).
:) Сам видел как "ничто не работает" без этого "volatile". :) :( :)
Добавление от того же "OU".
..и дело в тех случаях "было" совершенно не в многопоточности.
..и все тот же "OU"
Вообще-то во второй половине "Исходного сообщения" Алены уже приводилась цитата одного из заруюежных developers, что более научно и наглядно, чем я описал выше про IO и др.
И еще кое-что только что попалось (особенно не вникал, но похоже на правду ;-) :) ):
http://www.kalinin.ru/programming/cpp/12_09_00.shtml
Добрый день. Не являюсь программистом по специальности, но программировать надо.Из блога мало что понятно, а зачем я его читаю? Блог нашелся поисковиком на "volatile". Дело в том, что в книге Numerical Recipes (Press, Teukolski, Vetterling, Flannery 2002) наткнулась на это "volatile". Привожу вам отрывок оттуда, так как вы, по всей видимости, люди интересующиеся :) . Извините, уж без перевода, так как русской терминологией не владею. Речь идет об элементарном рассчете производной функции как конечной разности. В знаменателе имеем вариацию независимой переменной h, то есть
f'(x) ~= (f(x+h) - f(x)) / h.
Теперь собственно слова авторов: "...Always choose h so that x+h and x differ by an exactly representable number. This can usually be accomplished by the program step
"temp=x+h; h=temp-x (**)".
Some optimizing compilers, and some computers whose floating point chips have higher internal accuracy than is stored externally, can foil this trick; if so, it is usually enough to declare "temp" as "volatile", or esle to call a dummy function "donothing(temp)" between the two equations (**). This forces "temp" into and out of addressable memory.
Цитата:
"...Always choose h so that x+h and x differ by an exactly representable number. This can usually be accomplished by the program step
"temp=x+h; h=temp-x (**)".
Some optimizing compilers, and some computers whose floating point chips have higher internal accuracy than is stored externally, can foil this trick; if so, it is usually enough to declare "temp" as "volatile", or esle to call a dummy function "donothing(temp)" between the two equations (**). This forces "temp" into and out of addressable memory."
Насколько я понял, используя фокус наподобие "temp=x+h; h=temp-x;" автор предлагает получить h равным минимальному значению, на которое могут отличаться 2 числа, близкие к x.
Далее автор сокрушается по поводу того, что некоторые компы имеют математические сопроцессоры с высокой точностью представления чисел - с большей, чем если бы это число хранилось во внешней по отношению к процессору памяти. Дескать, с такими железками такой фокус не получится. Но обычно достаточно объявить переменную temp как volatile, или разместить некую
фиктивную (ничего не делающую, "donothing(temp)") функцию между двумя уравнениями, то этого будет достаточно, что бы переменная temp размещалась именно во внешней памяти, а не во внутренних регистрах процессора.
Теперь о том, для чего мне обычно нужен volatile.
Поскольку я иногда пишу программы для котороллера M16C/62, то приходится использовать обычный C,
поскольку компилятора C++ для него у меня нет (по моему, его и в природе нет). Нужен, когда:
1)
extern volatile int port_b; //port_b - это порт ввода-вывода, а не обычная переменная;
//Записать мы можем одно, а прочитать - другое, и тут любая
//оптимизация противопоказана:
port_b = x; //запись в порт b
x = port_b; //чтение из порта b
2)
extern volatile int port_b; //port_b - это порт ввода-вывода, а не обычная переменная;
static volatile bool port_b_can_read = false,
port_b_can_write = false;
void interrupt_read()
{ port_b_can_read = true; }
void interrupt_write()
{ port_b_can_write = true; }
void read_port_b(int *res)
{
while(!port_b_can_read)
wait();
*res = port_b;
}
void write_port_b(int data)
{
while(!port_b_can_write)
wait();
port_b = data;
}
Во втором примере volatile нужен, что бы исключить регистровую оптимизацию переменных
port_b_can_read и port_b_can_write. Иначе компилятор запихнет их в регистр и цикл будет
крутится бесконечно.
Недавно столкнулся с подтверждением необходимости volatile. Компилятор gcc-3.3.6, максимальная оптимизация.
Вот кусок кода на с++:
...
*ptr = *srcPtr;
mutex.leave();
...
Если переменные не волатильные, то оптимизатор выносил присвоение за выход из мьютекса.
А вот, если интересно, ассемблерные коды этого куска.
Это без volatile:
0x0806627d f0+93: mov 0x24(%ecx),%edx
0x08066280 f0+96: mov $0x1,%eax
0x08066285 f0+101: mov %eax,0x8083524
0x0806628a f0+106: mov %edx,0x18(%ecx)
Тут f0+93 и f0+106 - копирование значений, а f0+96 и f0+101 - выход из мьютекса. Как видно собственно операция копирования происходит после выхода из мьютекса.
Это с volatile:
0x08066b7b f0+123: mov 0x24(%ecx),%edi
0x08066b7e f0+126: mov $0x1,%eax
0x08066b83 f0+131: mov %edi,0x18(%ecx)
0x08066b86 f0+134: mov %eax,0x8083524
Собственно копирование f0+131 происходит до выхода из мьютекса f0+134.
2 Анонимный:
Недавно столкнулся с подтверждением необходимости volatile. Компилятор gcc-3.3.6, максимальная оптимизация.
А вот, если интересно, ассемблерные коды этого куска.
Да, интересно!
Много было споров по этому поводу, но смотреть ассемблерный код народ ленился.
Спасибо!
да уж )) налетел в gcc на это, долго не мог понять как же так, что за локальные копии глобальных переменных в цикле, уже начал было терять доверие к окружающему =) RTFM!
Случайно наткнулся на вашу заметку с рассуждением на тему "а зачем оно вообще нужно". Хочу привести пример, где volatile реально нужен и актулен в настоящее время. Это немного обособленная сфера, но очень распространённая в настоящее время - программирование микроконтроллеров. Здесь оптимизация без volatile может изрядно попортить нервы =) Вот, например, работа с gcc под платформу AVR
http://gremlinable.livejournal.com/6808.html
с другой стороны, кстати, под тот же AVR компилятор IAR вообще трактует volatile совершенно иначе:
Volatile storage
Data stored in a volatile storage device is not retained when the power to the device is turned off. In order to preserve data during a power-down cycle, you should store it in non-volatile storage. This should not be confused with the C keyword volatile.
2Alatar:
с другой стороны, кстати, под тот же AVR компилятор IAR вообще трактует volatile совершенно иначе
Спасибо, интересно очень.
Отправить комментарий